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