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