PubSubManager.java

  1. /**
  2.  *
  3.  * Copyright the original author or authors
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.pubsub;

  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.WeakHashMap;
  23. import java.util.concurrent.ConcurrentHashMap;
  24. import java.util.logging.Level;
  25. import java.util.logging.Logger;

  26. import org.jivesoftware.smack.Manager;
  27. import org.jivesoftware.smack.SmackException.NoResponseException;
  28. import org.jivesoftware.smack.SmackException.NotConnectedException;
  29. import org.jivesoftware.smack.XMPPConnection;
  30. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  31. import org.jivesoftware.smack.packet.EmptyResultIQ;
  32. import org.jivesoftware.smack.packet.ExtensionElement;
  33. import org.jivesoftware.smack.packet.IQ;
  34. import org.jivesoftware.smack.packet.IQ.Type;
  35. import org.jivesoftware.smack.packet.Stanza;
  36. import org.jivesoftware.smack.packet.StanzaError;
  37. import org.jivesoftware.smack.packet.StanzaError.Condition;

  38. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  39. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  40. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  41. import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
  42. import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
  43. import org.jivesoftware.smackx.pubsub.packet.PubSub;
  44. import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
  45. import org.jivesoftware.smackx.pubsub.util.NodeUtils;
  46. import org.jivesoftware.smackx.xdata.Form;
  47. import org.jivesoftware.smackx.xdata.FormField;

  48. import org.jxmpp.jid.BareJid;
  49. import org.jxmpp.jid.DomainBareJid;
  50. import org.jxmpp.jid.Jid;
  51. import org.jxmpp.jid.impl.JidCreate;
  52. import org.jxmpp.stringprep.XmppStringprepException;

  53. /**
  54.  * This is the starting point for access to the pubsub service.  It
  55.  * will provide access to general information about the service, as
  56.  * well as create or retrieve pubsub {@link LeafNode} instances.  These
  57.  * instances provide the bulk of the functionality as defined in the
  58.  * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>.
  59.  *
  60.  * @author Robin Collier
  61.  */
  62. public final class PubSubManager extends Manager {

  63.     public static final String AUTO_CREATE_FEATURE = "http://jabber.org/protocol/pubsub#auto-create";

  64.     private static final Logger LOGGER = Logger.getLogger(PubSubManager.class.getName());
  65.     private static final Map<XMPPConnection, Map<BareJid, PubSubManager>> INSTANCES = new WeakHashMap<>();

  66.     /**
  67.      * The JID of the PubSub service this manager manages.
  68.      */
  69.     private final BareJid pubSubService;

  70.     /**
  71.      * A map of node IDs to Nodes, used to cache those Nodes. This does only cache the type of Node,
  72.      * i.e. {@link CollectionNode} or {@link LeafNode}.
  73.      */
  74.     private final Map<String, Node> nodeMap = new ConcurrentHashMap<>();

  75.     /**
  76.      * Get a PubSub manager for the default PubSub service of the connection.
  77.      *
  78.      * @param connection
  79.      * @return the default PubSub manager.
  80.      */
  81.     public static PubSubManager getInstance(XMPPConnection connection) {
  82.         DomainBareJid pubSubService = null;
  83.         if (connection.isAuthenticated()) {
  84.             try {
  85.                 pubSubService = getPubSubService(connection);
  86.             }
  87.             catch (NoResponseException | XMPPErrorException | NotConnectedException e) {
  88.                 LOGGER.log(Level.WARNING, "Could not determine PubSub service", e);
  89.             }
  90.             catch (InterruptedException e) {
  91.                 LOGGER.log(Level.FINE, "Interrupted while trying to determine PubSub service", e);
  92.             }
  93.         }
  94.         if (pubSubService == null) {
  95.             try {
  96.                 // Perform an educated guess about what the PubSub service's domain bare JID may be
  97.                 pubSubService = JidCreate.domainBareFrom("pubsub." + connection.getXMPPServiceDomain());
  98.             }
  99.             catch (XmppStringprepException e) {
  100.                 throw new RuntimeException(e);
  101.             }
  102.         }
  103.         return getInstance(connection, pubSubService);
  104.     }

  105.     /**
  106.      * Get the PubSub manager for the given connection and PubSub service.
  107.      *
  108.      * @param connection the XMPP connection.
  109.      * @param pubSubService the PubSub service.
  110.      * @return a PubSub manager for the connection and service.
  111.      */
  112.     public static synchronized PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) {
  113.         Map<BareJid, PubSubManager> managers = INSTANCES.get(connection);
  114.         if (managers == null) {
  115.             managers = new HashMap<>();
  116.             INSTANCES.put(connection, managers);
  117.         }
  118.         PubSubManager pubSubManager = managers.get(pubSubService);
  119.         if (pubSubManager == null) {
  120.             pubSubManager = new PubSubManager(connection, pubSubService);
  121.             managers.put(pubSubService, pubSubManager);
  122.         }
  123.         return pubSubManager;
  124.     }

  125.     /**
  126.      * Create a pubsub manager associated to the specified connection where
  127.      * the pubsub requests require a specific to address for packets.
  128.      *
  129.      * @param connection The XMPP connection
  130.      * @param toAddress The pubsub specific to address (required for some servers)
  131.      */
  132.     PubSubManager(XMPPConnection connection, BareJid toAddress) {
  133.         super(connection);
  134.         pubSubService = toAddress;
  135.     }

  136.     /**
  137.      * Creates an instant node, if supported.
  138.      *
  139.      * @return The node that was created
  140.      * @throws XMPPErrorException
  141.      * @throws NoResponseException
  142.      * @throws NotConnectedException
  143.      * @throws InterruptedException
  144.      */
  145.     public LeafNode createNode() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  146.         PubSub reply = sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.CREATE), null);
  147.         NodeExtension elem = reply.getExtension("create", PubSubNamespace.basic.getXmlns());

  148.         LeafNode newNode = new LeafNode(this, elem.getNode());
  149.         nodeMap.put(newNode.getId(), newNode);

  150.         return newNode;
  151.     }

  152.     /**
  153.      * Creates a node with default configuration.
  154.      *
  155.      * @param nodeId The id of the node, which must be unique within the
  156.      * pubsub service
  157.      * @return The node that was created
  158.      * @throws XMPPErrorException
  159.      * @throws NoResponseException
  160.      * @throws NotConnectedException
  161.      * @throws InterruptedException
  162.      */
  163.     public LeafNode createNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  164.         return (LeafNode) createNode(nodeId, null);
  165.     }

  166.     /**
  167.      * Creates a node with specified configuration.
  168.      *
  169.      * Note: This is the only way to create a collection node.
  170.      *
  171.      * @param nodeId The name of the node, which must be unique within the
  172.      * pubsub service
  173.      * @param config The configuration for the node
  174.      * @return The node that was created
  175.      * @throws XMPPErrorException
  176.      * @throws NoResponseException
  177.      * @throws NotConnectedException
  178.      * @throws InterruptedException
  179.      */
  180.     public Node createNode(String nodeId, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  181.         PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId));
  182.         boolean isLeafNode = true;

  183.         if (config != null) {
  184.             request.addExtension(new FormNode(FormNodeType.CONFIGURE, config));
  185.             FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName());

  186.             if (nodeTypeField != null)
  187.                 isLeafNode = nodeTypeField.getValues().get(0).equals(NodeType.leaf.toString());
  188.         }

  189.         // Errors will cause exceptions in getReply, so it only returns
  190.         // on success.
  191.         sendPubsubPacket(request);
  192.         Node newNode = isLeafNode ? new LeafNode(this, nodeId) : new CollectionNode(this, nodeId);
  193.         nodeMap.put(newNode.getId(), newNode);

  194.         return newNode;
  195.     }

  196.     /**
  197.      * Retrieves the requested node, if it exists.  It will throw an
  198.      * exception if it does not.
  199.      *
  200.      * @param id - The unique id of the node
  201.      * @param <T> type of the node.
  202.      *
  203.      * @return the node
  204.      * @throws XMPPErrorException The node does not exist
  205.      * @throws NoResponseException if there was no response from the server.
  206.      * @throws NotConnectedException
  207.      * @throws InterruptedException
  208.      * @throws NotAPubSubNodeException
  209.      */
  210.     public <T extends Node> T getNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAPubSubNodeException {
  211.         Node node = nodeMap.get(id);

  212.         if (node == null) {
  213.             DiscoverInfo info = new DiscoverInfo();
  214.             info.setTo(pubSubService);
  215.             info.setNode(id);

  216.             DiscoverInfo infoReply = connection().createStanzaCollectorAndSend(info).nextResultOrThrow();

  217.             if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) {
  218.                 node = new LeafNode(this, id);
  219.             }
  220.             else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) {
  221.                 node = new CollectionNode(this, id);
  222.             }
  223.             else {
  224.                 throw new PubSubException.NotAPubSubNodeException(id, infoReply);
  225.             }
  226.             nodeMap.put(id, node);
  227.         }
  228.         @SuppressWarnings("unchecked")
  229.         T res = (T) node;
  230.         return res;
  231.     }

  232.     /**
  233.      * Try to get a leaf node and create one if it does not already exist.
  234.      *
  235.      * @param id The unique ID of the node.
  236.      * @return the leaf node.
  237.      * @throws NoResponseException
  238.      * @throws NotConnectedException
  239.      * @throws InterruptedException
  240.      * @throws XMPPErrorException
  241.      * @throws NotALeafNodeException in case the node already exists as collection node.
  242.      * @since 4.2.1
  243.      */
  244.     public LeafNode getOrCreateLeafNode(final String id)
  245.                     throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, NotALeafNodeException {
  246.         try {
  247.             return getNode(id);
  248.         }
  249.         catch (NotAPubSubNodeException e) {
  250.             return createNode(id);
  251.         }
  252.         catch (XMPPErrorException e1) {
  253.             if (e1.getStanzaError().getCondition() == Condition.item_not_found) {
  254.                 try {
  255.                     return createNode(id);
  256.                 }
  257.                 catch (XMPPErrorException e2) {
  258.                     if (e2.getStanzaError().getCondition() == Condition.conflict) {
  259.                         // The node was created in the meantime, re-try getNode(). Note that this case should be rare.
  260.                         try {
  261.                             return getNode(id);
  262.                         }
  263.                         catch (NotAPubSubNodeException e) {
  264.                             // Should not happen
  265.                             throw new IllegalStateException(e);
  266.                         }
  267.                     }
  268.                     throw e2;
  269.                 }
  270.             }
  271.             if (e1.getStanzaError().getCondition() == Condition.service_unavailable) {
  272.                 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not
  273.                 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or
  274.                 // collection node.
  275.                 LOGGER.warning("The PubSub service " + pubSubService
  276.                         + " threw an DiscoInfoNodeAssertionError, trying workaround for Prosody bug #805 (https://prosody.im/issues/issue/805)");
  277.                 return getOrCreateLeafNodeProsodyWorkaround(id);
  278.             }
  279.             throw e1;
  280.         }
  281.     }

  282.     /**
  283.      * Try to get a leaf node with the given node ID.
  284.      *
  285.      * @param id the node ID.
  286.      * @return the requested leaf node.
  287.      * @throws NotALeafNodeException in case the node exists but is a collection node.
  288.      * @throws NoResponseException
  289.      * @throws NotConnectedException
  290.      * @throws InterruptedException
  291.      * @throws XMPPErrorException
  292.      * @throws NotAPubSubNodeException
  293.      * @since 4.2.1
  294.      */
  295.     public LeafNode getLeafNode(String id) throws NotALeafNodeException, NoResponseException, NotConnectedException,
  296.                     InterruptedException, XMPPErrorException, NotAPubSubNodeException {
  297.         Node node;
  298.         try {
  299.             node = getNode(id);
  300.         }
  301.         catch (XMPPErrorException e) {
  302.             if (e.getStanzaError().getCondition() == Condition.service_unavailable) {
  303.                 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not
  304.                 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or
  305.                 // collection node.
  306.                 return getLeafNodeProsodyWorkaround(id);
  307.             }
  308.             throw e;
  309.         }

  310.         if (node instanceof LeafNode) {
  311.             return (LeafNode) node;
  312.         }

  313.         throw new PubSubException.NotALeafNodeException(id, pubSubService);
  314.     }

  315.     private LeafNode getLeafNodeProsodyWorkaround(final String id) throws NoResponseException, NotConnectedException,
  316.                     InterruptedException, NotALeafNodeException, XMPPErrorException {
  317.         LeafNode leafNode = new LeafNode(this, id);
  318.         try {
  319.             // Try to ensure that this is not a collection node by asking for one item form the node.
  320.             leafNode.getItems(1);
  321.         } catch (XMPPErrorException e) {
  322.             Condition condition = e.getStanzaError().getCondition();
  323.             if (condition == Condition.feature_not_implemented) {
  324.                 // XEP-0060 § 6.5.9.5: Item retrieval not supported, e.g. because node is a collection node
  325.                 throw new PubSubException.NotALeafNodeException(id, pubSubService);
  326.             }

  327.             throw e;
  328.         }

  329.         nodeMap.put(id, leafNode);

  330.         return leafNode;
  331.     }

  332.     private LeafNode getOrCreateLeafNodeProsodyWorkaround(final String id)
  333.                     throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotALeafNodeException {
  334.         try {
  335.             return createNode(id);
  336.         }
  337.         catch (XMPPErrorException e1) {
  338.             if (e1.getStanzaError().getCondition() == Condition.conflict) {
  339.                 return getLeafNodeProsodyWorkaround(id);
  340.             }
  341.             throw e1;
  342.         }
  343.     }

  344.     /**
  345.      * Try to publish an item and, if the node with the given ID does not exists, auto-create the node.
  346.      * <p>
  347.      * Not every PubSub service supports automatic node creation. You can discover if this service supports it by using
  348.      * {@link #supportsAutomaticNodeCreation()}.
  349.      * </p>
  350.      *
  351.      * @param id The unique id of the node.
  352.      * @param item The item to publish.
  353.      * @param <I> type of the item.
  354.      *
  355.      * @return the LeafNode on which the item was published.
  356.      * @throws NoResponseException
  357.      * @throws XMPPErrorException
  358.      * @throws NotConnectedException
  359.      * @throws InterruptedException
  360.      * @since 4.2.1
  361.      */
  362.     public <I extends Item> LeafNode tryToPublishAndPossibleAutoCreate(String id, I item)
  363.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  364.         LeafNode leafNode = new LeafNode(this, id);
  365.         leafNode.publish(item);

  366.         // If LeafNode.publish() did not throw then we have successfully published an item and possible auto-created
  367.         // (XEP-0163 § 3., XEP-0060 § 7.1.4) the node. So we can put the node into the nodeMap.
  368.         nodeMap.put(id, leafNode);

  369.         return leafNode;
  370.     }

  371.     /**
  372.      * Get all the nodes that currently exist as a child of the specified
  373.      * collection node.  If the service does not support collection nodes
  374.      * then all nodes will be returned.
  375.      *
  376.      * To retrieve contents of the root collection node (if it exists),
  377.      * or there is no root collection node, pass null as the nodeId.
  378.      *
  379.      * @param nodeId - The id of the collection node for which the child
  380.      * nodes will be returned.
  381.      * @return {@link DiscoverItems} representing the existing nodes
  382.      * @throws XMPPErrorException
  383.      * @throws NoResponseException if there was no response from the server.
  384.      * @throws NotConnectedException
  385.      * @throws InterruptedException
  386.      */
  387.     public DiscoverItems discoverNodes(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  388.         DiscoverItems items = new DiscoverItems();

  389.         if (nodeId != null)
  390.             items.setNode(nodeId);
  391.         items.setTo(pubSubService);
  392.         DiscoverItems nodeItems = connection().createStanzaCollectorAndSend(items).nextResultOrThrow();
  393.         return nodeItems;
  394.     }

  395.     /**
  396.      * Gets the subscriptions on the root node.
  397.      *
  398.      * @return List of exceptions
  399.      * @throws XMPPErrorException
  400.      * @throws NoResponseException
  401.      * @throws NotConnectedException
  402.      * @throws InterruptedException
  403.      */
  404.     public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  405.         Stanza reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS), null);
  406.         SubscriptionsExtension subElem = reply.getExtension(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns());
  407.         return subElem.getSubscriptions();
  408.     }

  409.     /**
  410.      * Gets the affiliations on the root node.
  411.      *
  412.      * @return List of affiliations
  413.      * @throws XMPPErrorException
  414.      * @throws NoResponseException
  415.      * @throws NotConnectedException
  416.      * @throws InterruptedException
  417.      *
  418.      */
  419.     public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  420.         PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS), null);
  421.         AffiliationsExtension listElem = reply.getExtension(PubSubElementType.AFFILIATIONS);
  422.         return listElem.getAffiliations();
  423.     }

  424.     /**
  425.      * Delete the specified node.
  426.      *
  427.      * @param nodeId
  428.      * @throws XMPPErrorException
  429.      * @throws NoResponseException
  430.      * @throws NotConnectedException
  431.      * @throws InterruptedException
  432.      */
  433.     public void deleteNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  434.         sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace());
  435.         nodeMap.remove(nodeId);
  436.     }

  437.     /**
  438.      * Returns the default settings for Node configuration.
  439.      *
  440.      * @return configuration form containing the default settings.
  441.      * @throws XMPPErrorException
  442.      * @throws NoResponseException
  443.      * @throws NotConnectedException
  444.      * @throws InterruptedException
  445.      */
  446.     public ConfigureForm getDefaultConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  447.         // Errors will cause exceptions in getReply, so it only returns
  448.         // on success.
  449.         PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace());
  450.         return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT);
  451.     }

  452.     /**
  453.      * Get the JID of the PubSub service managed by this manager.
  454.      *
  455.      * @return the JID of the PubSub service.
  456.      */
  457.     public BareJid getServiceJid() {
  458.         return pubSubService;
  459.     }

  460.     /**
  461.      * Gets the supported features of the servers pubsub implementation
  462.      * as a standard {@link DiscoverInfo} instance.
  463.      *
  464.      * @return The supported features
  465.      * @throws XMPPErrorException
  466.      * @throws NoResponseException
  467.      * @throws NotConnectedException
  468.      * @throws InterruptedException
  469.      */
  470.     public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  471.         ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(connection());
  472.         return mgr.discoverInfo(pubSubService);
  473.     }

  474.     /**
  475.      * Check if the PubSub service supports automatic node creation.
  476.      *
  477.      * @return true if the PubSub service supports automatic node creation.
  478.      * @throws NoResponseException
  479.      * @throws XMPPErrorException
  480.      * @throws NotConnectedException
  481.      * @throws InterruptedException
  482.      * @since 4.2.1
  483.      * @see <a href="https://xmpp.org/extensions/xep-0060.html#publisher-publish-autocreate">XEP-0060 § 7.1.4 Automatic Node Creation</a>
  484.      */
  485.     public boolean supportsAutomaticNodeCreation()
  486.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  487.         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection());
  488.         return sdm.supportsFeature(pubSubService, AUTO_CREATE_FEATURE);
  489.     }

  490.     /**
  491.      * Check if it is possible to create PubSub nodes on this service. It could be possible that the
  492.      * PubSub service allows only certain XMPP entities (clients) to create nodes and publish items
  493.      * to them.
  494.      * <p>
  495.      * Note that since XEP-60 does not provide an API to determine if an XMPP entity is allowed to
  496.      * create nodes, therefore this method creates an instant node calling {@link #createNode()} to
  497.      * determine if it is possible to create nodes.
  498.      * </p>
  499.      *
  500.      * @return <code>true</code> if it is possible to create nodes, <code>false</code> otherwise.
  501.      * @throws NoResponseException
  502.      * @throws NotConnectedException
  503.      * @throws InterruptedException
  504.      * @throws XMPPErrorException
  505.      */
  506.     public boolean canCreateNodesAndPublishItems() throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException {
  507.         LeafNode leafNode = null;
  508.         try {
  509.             leafNode = createNode();
  510.         }
  511.         catch (XMPPErrorException e) {
  512.             if (e.getStanzaError().getCondition() == StanzaError.Condition.forbidden) {
  513.                 return false;
  514.             }
  515.             throw e;
  516.         } finally {
  517.             if (leafNode != null) {
  518.                 deleteNode(leafNode.getId());
  519.             }
  520.         }
  521.         return true;
  522.     }

  523.     private PubSub sendPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns)
  524.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  525.         return sendPubsubPacket(pubSubService, type, Collections.singletonList(ext), ns);
  526.     }

  527.     XMPPConnection getConnection() {
  528.         return connection();
  529.     }

  530.     PubSub sendPubsubPacket(Jid to, Type type, List<ExtensionElement> extList, PubSubNamespace ns)
  531.                     throws NoResponseException, XMPPErrorException, NotConnectedException,
  532.                     InterruptedException {
  533. // CHECKSTYLE:OFF
  534.         PubSub pubSub = new PubSub(to, type, ns);
  535.         for (ExtensionElement pe : extList) {
  536.             pubSub.addExtension(pe);
  537.         }
  538. // CHECKSTYLE:ON
  539.         return sendPubsubPacket(pubSub);
  540.     }

  541.     PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException,
  542.                     NotConnectedException, InterruptedException {
  543.         IQ resultIQ = connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
  544.         if (resultIQ instanceof EmptyResultIQ) {
  545.             return null;
  546.         }
  547.         return (PubSub) resultIQ;
  548.     }

  549.     /**
  550.      * Get the "default" PubSub service for a given XMPP connection. The default PubSub service is
  551.      * simply an arbitrary XMPP service with the PubSub feature and an identity of category "pubsub"
  552.      * and type "service".
  553.      *
  554.      * @param connection
  555.      * @return the default PubSub service or <code>null</code>.
  556.      * @throws NoResponseException
  557.      * @throws XMPPErrorException
  558.      * @throws NotConnectedException
  559.      * @throws InterruptedException
  560.      * @see <a href="http://xmpp.org/extensions/xep-0060.html#entity-features">XEP-60 § 5.1 Discover
  561.      *      Features</a>
  562.      */
  563.     public static DomainBareJid getPubSubService(XMPPConnection connection)
  564.                     throws NoResponseException, XMPPErrorException, NotConnectedException,
  565.                     InterruptedException {
  566.         return ServiceDiscoveryManager.getInstanceFor(connection).findService(PubSub.NAMESPACE,
  567.                         true, "pubsub", "service");
  568.     }
  569. }