001/** 002 * 003 * Copyright the original author or authors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.pubsub; 018 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.WeakHashMap; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import javax.xml.namespace.QName; 029 030import org.jivesoftware.smack.Manager; 031import org.jivesoftware.smack.SmackException.NoResponseException; 032import org.jivesoftware.smack.SmackException.NotConnectedException; 033import org.jivesoftware.smack.XMPPConnection; 034import org.jivesoftware.smack.XMPPException.XMPPErrorException; 035import org.jivesoftware.smack.packet.EmptyResultIQ; 036import org.jivesoftware.smack.packet.ExtensionElement; 037import org.jivesoftware.smack.packet.IQ; 038import org.jivesoftware.smack.packet.IQ.Type; 039import org.jivesoftware.smack.packet.Stanza; 040import org.jivesoftware.smack.packet.StanzaError; 041import org.jivesoftware.smack.packet.StanzaError.Condition; 042import org.jivesoftware.smack.util.StringUtils; 043 044import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 045import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 046import org.jivesoftware.smackx.disco.packet.DiscoverItems; 047import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; 048import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException; 049import org.jivesoftware.smackx.pubsub.form.ConfigureForm; 050import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm; 051import org.jivesoftware.smackx.pubsub.packet.PubSub; 052import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; 053import org.jivesoftware.smackx.pubsub.util.NodeUtils; 054import org.jivesoftware.smackx.xdata.packet.DataForm; 055 056import org.jxmpp.jid.BareJid; 057import org.jxmpp.jid.DomainBareJid; 058import org.jxmpp.jid.Jid; 059import org.jxmpp.jid.impl.JidCreate; 060import org.jxmpp.stringprep.XmppStringprepException; 061 062/** 063 * This is the starting point for access to the pubsub service. It 064 * will provide access to general information about the service, as 065 * well as create or retrieve pubsub {@link LeafNode} instances. These 066 * instances provide the bulk of the functionality as defined in the 067 * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>. 068 * 069 * @author Robin Collier 070 */ 071public final class PubSubManager extends Manager { 072 073 public static final String PLUS_NOTIFY = "+notify"; 074 075 public static final String AUTO_CREATE_FEATURE = "http://jabber.org/protocol/pubsub#auto-create"; 076 077 private static final Logger LOGGER = Logger.getLogger(PubSubManager.class.getName()); 078 private static final Map<XMPPConnection, Map<BareJid, PubSubManager>> INSTANCES = new WeakHashMap<>(); 079 080 /** 081 * The JID of the PubSub service this manager manages. 082 */ 083 private final BareJid pubSubService; 084 085 /** 086 * A map of node IDs to Nodes, used to cache those Nodes. This does only cache the type of Node, 087 * i.e. {@link CollectionNode} or {@link LeafNode}. 088 */ 089 private final Map<String, Node> nodeMap = new ConcurrentHashMap<>(); 090 091 /** 092 * Get a PubSub manager for the default PubSub service of the connection. 093 * 094 * @param connection TODO javadoc me please 095 * @return the default PubSub manager. 096 */ 097 // CHECKSTYLE:OFF:RegexpSingleline 098 public static PubSubManager getInstanceFor(XMPPConnection connection) { 099 // CHECKSTYLE:ON:RegexpSingleline 100 DomainBareJid pubSubService = null; 101 if (connection.isAuthenticated()) { 102 try { 103 pubSubService = getPubSubService(connection); 104 } 105 catch (NoResponseException | XMPPErrorException | NotConnectedException e) { 106 LOGGER.log(Level.WARNING, "Could not determine PubSub service", e); 107 } 108 catch (InterruptedException e) { 109 LOGGER.log(Level.FINE, "Interrupted while trying to determine PubSub service", e); 110 } 111 } 112 if (pubSubService == null) { 113 try { 114 // Perform an educated guess about what the PubSub service's domain bare JID may be 115 pubSubService = JidCreate.domainBareFrom("pubsub." + connection.getXMPPServiceDomain()); 116 } 117 catch (XmppStringprepException e) { 118 throw new RuntimeException(e); 119 } 120 } 121 return getInstanceFor(connection, pubSubService); 122 } 123 124 /** 125 * Get the PubSub manager for the given connection and PubSub service. Use <code>null</code> as argument for 126 * pubSubService to retrieve a PubSubManager for the users PEP service. 127 * 128 * @param connection the XMPP connection. 129 * @param pubSubService the PubSub service, may be <code>null</code>. 130 * @return a PubSub manager for the connection and service. 131 */ 132 // CHECKSTYLE:OFF:RegexpSingleline 133 public static PubSubManager getInstanceFor(XMPPConnection connection, BareJid pubSubService) { 134 // CHECKSTYLE:ON:RegexpSingleline 135 if (pubSubService != null && connection.isAuthenticated() && connection.getUser().asBareJid().equals(pubSubService)) { 136 // PEP service. 137 pubSubService = null; 138 } 139 140 PubSubManager pubSubManager; 141 Map<BareJid, PubSubManager> managers; 142 synchronized (INSTANCES) { 143 managers = INSTANCES.get(connection); 144 if (managers == null) { 145 managers = new HashMap<>(); 146 INSTANCES.put(connection, managers); 147 } 148 } 149 synchronized (managers) { 150 pubSubManager = managers.get(pubSubService); 151 if (pubSubManager == null) { 152 pubSubManager = new PubSubManager(connection, pubSubService); 153 managers.put(pubSubService, pubSubManager); 154 } 155 } 156 157 return pubSubManager; 158 } 159 160 /** 161 * Deprecated. 162 * 163 * @param connection the connection. 164 * @return the PubSub manager for the given connection. 165 * @deprecated use {@link #getInstanceFor(XMPPConnection)} instead. 166 */ 167 @Deprecated 168 // TODO: Remove in Smack 4.5. 169 public static PubSubManager getInstance(XMPPConnection connection) { 170 return getInstanceFor(connection); 171 } 172 173 /** 174 * Deprecated. 175 * 176 * @param connection the connection. 177 * @param pubSubService the XMPP address of the PubSub service. 178 * @return the PubSub manager for the given connection. 179 * @deprecated use {@link #getInstanceFor(XMPPConnection, BareJid)} instead. 180 */ 181 @Deprecated 182 // TODO: Remove in Smack 4.5. 183 public static PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) { 184 return getInstanceFor(connection, pubSubService); 185 } 186 187 /** 188 * Create a pubsub manager associated to the specified connection where 189 * the pubsub requests require a specific to address for packets. 190 * 191 * @param connection The XMPP connection 192 * @param toAddress The pubsub specific to address (required for some servers) 193 */ 194 PubSubManager(XMPPConnection connection, BareJid toAddress) { 195 super(connection); 196 pubSubService = toAddress; 197 } 198 199 private void checkIfXmppErrorBecauseOfNotLeafNode(String nodeId, XMPPErrorException xmppErrorException) 200 throws XMPPErrorException, NotALeafNodeException { 201 Condition condition = xmppErrorException.getStanzaError().getCondition(); 202 if (condition == Condition.feature_not_implemented) { 203 // XEP-0060 § 6.5.9.5: Item retrieval not supported, e.g. because node is a collection node 204 throw new PubSubException.NotALeafNodeException(nodeId, pubSubService); 205 } 206 207 throw xmppErrorException; 208 } 209 210 /** 211 * Creates an instant node, if supported. 212 * 213 * @return The node that was created 214 * @throws XMPPErrorException if there was an XMPP error returned. 215 * @throws NoResponseException if there was no response from the remote entity. 216 * @throws NotConnectedException if the XMPP connection is not connected. 217 * @throws InterruptedException if the calling thread was interrupted. 218 */ 219 public LeafNode createNode() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 220 PubSub reply = sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.CREATE), null); 221 QName qname = new QName(PubSubNamespace.basic.getXmlns(), "create"); 222 NodeExtension elem = (NodeExtension) reply.getExtension(qname); 223 224 LeafNode newNode = new LeafNode(this, elem.getNode()); 225 nodeMap.put(newNode.getId(), newNode); 226 227 return newNode; 228 } 229 230 /** 231 * Creates a node with default configuration. 232 * 233 * @param nodeId The id of the node, which must be unique within the 234 * pubsub service 235 * @return The node that was created 236 * @throws XMPPErrorException if there was an XMPP error returned. 237 * @throws NoResponseException if there was no response from the remote entity. 238 * @throws NotConnectedException if the XMPP connection is not connected. 239 * @throws InterruptedException if the calling thread was interrupted. 240 */ 241 public LeafNode createNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 242 return (LeafNode) createNode(nodeId, null); 243 } 244 245 /** 246 * Creates a node with specified configuration. 247 * 248 * Note: This is the only way to create a collection node. 249 * 250 * @param nodeId The name of the node, which must be unique within the 251 * pubsub service 252 * @param config The configuration for the node 253 * @return The node that was created 254 * @throws XMPPErrorException if there was an XMPP error returned. 255 * @throws NoResponseException if there was no response from the remote entity. 256 * @throws NotConnectedException if the XMPP connection is not connected. 257 * @throws InterruptedException if the calling thread was interrupted. 258 */ 259 public Node createNode(String nodeId, FillableConfigureForm config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 260 PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId)); 261 boolean isLeafNode = true; 262 263 if (config != null) { 264 DataForm submitForm = config.getDataFormToSubmit(); 265 request.addExtension(new FormNode(FormNodeType.CONFIGURE, submitForm)); 266 NodeType nodeType = config.getNodeType(); 267 // Note that some implementations do to have the pubsub#node_type field in their defauilt configuration, 268 // which I believe to be a bug. However, since PubSub specifies the default node type to be 'leaf' we assume 269 // leaf if the field does not exist. 270 isLeafNode = nodeType == null || nodeType == NodeType.leaf; 271 } 272 273 // Errors will cause exceptions in getReply, so it only returns 274 // on success. 275 sendPubsubPacket(request); 276 Node newNode = isLeafNode ? new LeafNode(this, nodeId) : new CollectionNode(this, nodeId); 277 nodeMap.put(newNode.getId(), newNode); 278 279 return newNode; 280 } 281 282 /** 283 * Retrieves the requested node, if it exists. It will throw an 284 * exception if it does not. 285 * 286 * @param id - The unique id of the node 287 * 288 * @return the node 289 * @throws XMPPErrorException The node does not exist 290 * @throws NoResponseException if there was no response from the server. 291 * @throws NotConnectedException if the XMPP connection is not connected. 292 * @throws InterruptedException if the calling thread was interrupted. 293 * @throws NotAPubSubNodeException if a involved node is not a PubSub node. 294 */ 295 public Node getNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAPubSubNodeException { 296 StringUtils.requireNotNullNorEmpty(id, "The node ID can not be null or the empty string"); 297 Node node = nodeMap.get(id); 298 299 if (node == null) { 300 XMPPConnection connection = connection(); 301 DiscoverInfo info = DiscoverInfo.builder(connection) 302 .to(pubSubService) 303 .setNode(id) 304 .build(); 305 306 DiscoverInfo infoReply = connection.createStanzaCollectorAndSend(info).nextResultOrThrow(); 307 308 if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) { 309 node = new LeafNode(this, id); 310 } 311 else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) { 312 node = new CollectionNode(this, id); 313 } 314 else { 315 throw new PubSubException.NotAPubSubNodeException(id, infoReply); 316 } 317 nodeMap.put(id, node); 318 } 319 return node; 320 } 321 322 /** 323 * Try to get a leaf node and create one if it does not already exist. 324 * 325 * @param id The unique ID of the node. 326 * @return the leaf node. 327 * @throws NoResponseException if there was no response from the remote entity. 328 * @throws NotConnectedException if the XMPP connection is not connected. 329 * @throws InterruptedException if the calling thread was interrupted. 330 * @throws XMPPErrorException if there was an XMPP error returned. 331 * @throws NotALeafNodeException in case the node already exists as collection node. 332 * @since 4.2.1 333 */ 334 public LeafNode getOrCreateLeafNode(final String id) 335 throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, NotALeafNodeException { 336 try { 337 return getLeafNode(id); 338 } 339 catch (NotAPubSubNodeException e) { 340 return createNode(id); 341 } 342 catch (XMPPErrorException e1) { 343 if (e1.getStanzaError().getCondition() == Condition.item_not_found) { 344 try { 345 return createNode(id); 346 } 347 catch (XMPPErrorException e2) { 348 if (e2.getStanzaError().getCondition() == Condition.conflict) { 349 // The node was created in the meantime, re-try getNode(). Note that this case should be rare. 350 try { 351 return getLeafNode(id); 352 } 353 catch (NotAPubSubNodeException e) { 354 // Should not happen 355 throw new IllegalStateException(e); 356 } 357 } 358 throw e2; 359 } 360 } 361 if (e1.getStanzaError().getCondition() == Condition.service_unavailable) { 362 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not 363 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or 364 // collection node. 365 LOGGER.warning("The PubSub service " + pubSubService 366 + " threw an DiscoInfoNodeAssertionError, trying workaround for Prosody bug #805 (https://prosody.im/issues/issue/805)"); 367 return getOrCreateLeafNodeProsodyWorkaround(id); 368 } 369 throw e1; 370 } 371 } 372 373 /** 374 * Try to get a leaf node with the given node ID. 375 * 376 * @param id the node ID. 377 * @return the requested leaf node. 378 * @throws NotALeafNodeException in case the node exists but is a collection node. 379 * @throws NoResponseException if there was no response from the remote entity. 380 * @throws NotConnectedException if the XMPP connection is not connected. 381 * @throws InterruptedException if the calling thread was interrupted. 382 * @throws XMPPErrorException if there was an XMPP error returned. 383 * @throws NotAPubSubNodeException if a involved node is not a PubSub node. 384 * @since 4.2.1 385 */ 386 public LeafNode getLeafNode(String id) throws NotALeafNodeException, NoResponseException, NotConnectedException, 387 InterruptedException, XMPPErrorException, NotAPubSubNodeException { 388 Node node; 389 try { 390 node = getNode(id); 391 } 392 catch (XMPPErrorException e) { 393 if (e.getStanzaError().getCondition() == Condition.service_unavailable) { 394 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not 395 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or 396 // collection node. 397 return getLeafNodeProsodyWorkaround(id); 398 } 399 throw e; 400 } 401 402 if (node instanceof LeafNode) { 403 return (LeafNode) node; 404 } 405 406 throw new PubSubException.NotALeafNodeException(id, pubSubService); 407 } 408 409 private LeafNode getLeafNodeProsodyWorkaround(final String id) throws NoResponseException, NotConnectedException, 410 InterruptedException, NotALeafNodeException, XMPPErrorException { 411 LeafNode leafNode = new LeafNode(this, id); 412 try { 413 // Try to ensure that this is not a collection node by asking for one item form the node. 414 leafNode.getItems(1); 415 } catch (XMPPErrorException e) { 416 checkIfXmppErrorBecauseOfNotLeafNode(id, e); 417 } 418 419 nodeMap.put(id, leafNode); 420 421 return leafNode; 422 } 423 424 private LeafNode getOrCreateLeafNodeProsodyWorkaround(final String id) 425 throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotALeafNodeException { 426 try { 427 return createNode(id); 428 } 429 catch (XMPPErrorException e1) { 430 if (e1.getStanzaError().getCondition() == Condition.conflict) { 431 return getLeafNodeProsodyWorkaround(id); 432 } 433 throw e1; 434 } 435 } 436 437 /** 438 * Try to publish an item and, if the node with the given ID does not exists, auto-create the node. 439 * <p> 440 * Not every PubSub service supports automatic node creation. You can discover if this service supports it by using 441 * {@link #supportsAutomaticNodeCreation()}. 442 * </p> 443 * 444 * @param id The unique id of the node. 445 * @param item The item to publish. 446 * @param <I> type of the item. 447 * 448 * @return the LeafNode on which the item was published. 449 * @throws NoResponseException if there was no response from the remote entity. 450 * @throws XMPPErrorException if there was an XMPP error returned. 451 * @throws NotConnectedException if the XMPP connection is not connected. 452 * @throws InterruptedException if the calling thread was interrupted. 453 * @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 454 * @since 4.2.1 455 */ 456 public <I extends Item> LeafNode tryToPublishAndPossibleAutoCreate(String id, I item) 457 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, 458 NotALeafNodeException { 459 LeafNode leafNode = new LeafNode(this, id); 460 461 try { 462 leafNode.publish(item); 463 } catch (XMPPErrorException e) { 464 checkIfXmppErrorBecauseOfNotLeafNode(id, e); 465 } 466 467 // If LeafNode.publish() did not throw then we have successfully published an item and possible auto-created 468 // (XEP-0163 § 3., XEP-0060 § 7.1.4) the node. So we can put the node into the nodeMap. 469 nodeMap.put(id, leafNode); 470 471 return leafNode; 472 } 473 474 /** 475 * Get all the nodes that currently exist as a child of the specified 476 * collection node. If the service does not support collection nodes 477 * then all nodes will be returned. 478 * 479 * To retrieve contents of the root collection node (if it exists), 480 * or there is no root collection node, pass null as the nodeId. 481 * 482 * @param nodeId - The id of the collection node for which the child 483 * nodes will be returned. 484 * @return {@link DiscoverItems} representing the existing nodes 485 * @throws XMPPErrorException if there was an XMPP error returned. 486 * @throws NoResponseException if there was no response from the server. 487 * @throws NotConnectedException if the XMPP connection is not connected. 488 * @throws InterruptedException if the calling thread was interrupted. 489 */ 490 public DiscoverItems discoverNodes(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 491 DiscoverItems items = new DiscoverItems(); 492 493 if (nodeId != null) 494 items.setNode(nodeId); 495 items.setTo(pubSubService); 496 DiscoverItems nodeItems = connection().createStanzaCollectorAndSend(items).nextResultOrThrow(); 497 return nodeItems; 498 } 499 500 /** 501 * Gets the subscriptions on the root node. 502 * 503 * @return List of exceptions 504 * @throws XMPPErrorException if there was an XMPP error returned. 505 * @throws NoResponseException if there was no response from the remote entity. 506 * @throws NotConnectedException if the XMPP connection is not connected. 507 * @throws InterruptedException if the calling thread was interrupted. 508 */ 509 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 510 Stanza reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS), null); 511 SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtensionElement(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns()); 512 return subElem.getSubscriptions(); 513 } 514 515 /** 516 * Gets the affiliations on the root node. 517 * 518 * @return List of affiliations 519 * @throws XMPPErrorException if there was an XMPP error returned. 520 * @throws NoResponseException if there was no response from the remote entity. 521 * @throws NotConnectedException if the XMPP connection is not connected. 522 * @throws InterruptedException if the calling thread was interrupted. 523 * 524 */ 525 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 526 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS), null); 527 AffiliationsExtension listElem = reply.getExtension(PubSubElementType.AFFILIATIONS); 528 return listElem.getAffiliations(); 529 } 530 531 /** 532 * Delete the specified node. 533 * 534 * @param nodeId TODO javadoc me please 535 * @throws XMPPErrorException if there was an XMPP error returned. 536 * @throws NoResponseException if there was no response from the remote entity. 537 * @throws NotConnectedException if the XMPP connection is not connected. 538 * @throws InterruptedException if the calling thread was interrupted. 539 * @return <code>true</code> if this node existed and was deleted and <code>false</code> if this node did not exist. 540 */ 541 public boolean deleteNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 542 boolean res = true; 543 try { 544 sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace()); 545 } catch (XMPPErrorException e) { 546 if (e.getStanzaError().getCondition() == StanzaError.Condition.item_not_found) { 547 res = false; 548 } else { 549 throw e; 550 } 551 } 552 nodeMap.remove(nodeId); 553 return res; 554 } 555 556 /** 557 * Returns the default settings for Node configuration. 558 * 559 * @return configuration form containing the default settings. 560 * @throws XMPPErrorException if there was an XMPP error returned. 561 * @throws NoResponseException if there was no response from the remote entity. 562 * @throws NotConnectedException if the XMPP connection is not connected. 563 * @throws InterruptedException if the calling thread was interrupted. 564 */ 565 public ConfigureForm getDefaultConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 566 // Errors will cause exceptions in getReply, so it only returns 567 // on success. 568 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace()); 569 return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT); 570 } 571 572 /** 573 * Get the JID of the PubSub service managed by this manager. 574 * 575 * @return the JID of the PubSub service. 576 */ 577 public BareJid getServiceJid() { 578 return pubSubService; 579 } 580 581 /** 582 * Gets the supported features of the servers pubsub implementation 583 * as a standard {@link DiscoverInfo} instance. 584 * 585 * @return The supported features 586 * @throws XMPPErrorException if there was an XMPP error returned. 587 * @throws NoResponseException if there was no response from the remote entity. 588 * @throws NotConnectedException if the XMPP connection is not connected. 589 * @throws InterruptedException if the calling thread was interrupted. 590 */ 591 public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 592 ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(connection()); 593 return mgr.discoverInfo(pubSubService); 594 } 595 596 /** 597 * Check if the PubSub service supports automatic node creation. 598 * 599 * @return true if the PubSub service supports automatic node creation. 600 * @throws NoResponseException if there was no response from the remote entity. 601 * @throws XMPPErrorException if there was an XMPP error returned. 602 * @throws NotConnectedException if the XMPP connection is not connected. 603 * @throws InterruptedException if the calling thread was interrupted. 604 * @since 4.2.1 605 * @see <a href="https://xmpp.org/extensions/xep-0060.html#publisher-publish-autocreate">XEP-0060 § 7.1.4 Automatic Node Creation</a> 606 */ 607 public boolean supportsAutomaticNodeCreation() 608 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 609 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection()); 610 return sdm.supportsFeature(pubSubService, AUTO_CREATE_FEATURE); 611 } 612 613 /** 614 * Check if it is possible to create PubSub nodes on this service. It could be possible that the 615 * PubSub service allows only certain XMPP entities (clients) to create nodes and publish items 616 * to them. 617 * <p> 618 * Note that since XEP-60 does not provide an API to determine if an XMPP entity is allowed to 619 * create nodes, therefore this method creates an instant node calling {@link #createNode()} to 620 * determine if it is possible to create nodes. 621 * </p> 622 * 623 * @return <code>true</code> if it is possible to create nodes, <code>false</code> otherwise. 624 * @throws NoResponseException if there was no response from the remote entity. 625 * @throws NotConnectedException if the XMPP connection is not connected. 626 * @throws InterruptedException if the calling thread was interrupted. 627 * @throws XMPPErrorException if there was an XMPP error returned. 628 */ 629 public boolean canCreateNodesAndPublishItems() throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException { 630 LeafNode leafNode = null; 631 try { 632 leafNode = createNode(); 633 } 634 catch (XMPPErrorException e) { 635 if (e.getStanzaError().getCondition() == StanzaError.Condition.forbidden) { 636 return false; 637 } 638 throw e; 639 } finally { 640 if (leafNode != null) { 641 deleteNode(leafNode.getId()); 642 } 643 } 644 return true; 645 } 646 647 private PubSub sendPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 648 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 649 return sendPubsubPacket(pubSubService, type, Collections.singletonList(ext), ns); 650 } 651 652 XMPPConnection getConnection() { 653 return connection(); 654 } 655 656 PubSub sendPubsubPacket(Jid to, Type type, List<ExtensionElement> extList, PubSubNamespace ns) 657 throws NoResponseException, XMPPErrorException, NotConnectedException, 658 InterruptedException { 659// CHECKSTYLE:OFF 660 PubSub pubSub = new PubSub(to, type, ns); 661 for (ExtensionElement pe : extList) { 662 pubSub.addExtension(pe); 663 } 664// CHECKSTYLE:ON 665 return sendPubsubPacket(pubSub); 666 } 667 668 PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, 669 NotConnectedException, InterruptedException { 670 IQ resultIQ = connection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); 671 if (resultIQ instanceof EmptyResultIQ) { 672 return null; 673 } 674 return (PubSub) resultIQ; 675 } 676 677 /** 678 * Get the "default" PubSub service for a given XMPP connection. The default PubSub service is 679 * simply an arbitrary XMPP service with the PubSub feature and an identity of category "pubsub" 680 * and type "service". 681 * 682 * @param connection TODO javadoc me please 683 * @return the default PubSub service or <code>null</code>. 684 * @throws NoResponseException if there was no response from the remote entity. 685 * @throws XMPPErrorException if there was an XMPP error returned. 686 * @throws NotConnectedException if the XMPP connection is not connected. 687 * @throws InterruptedException if the calling thread was interrupted. 688 * @see <a href="http://xmpp.org/extensions/xep-0060.html#entity-features">XEP-60 § 5.1 Discover 689 * Features</a> 690 */ 691 public static DomainBareJid getPubSubService(XMPPConnection connection) 692 throws NoResponseException, XMPPErrorException, NotConnectedException, 693 InterruptedException { 694 return ServiceDiscoveryManager.getInstanceFor(connection).findService(PubSub.NAMESPACE, 695 true, "pubsub", "service"); 696 } 697}