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 * Create a pubsub manager associated to the specified connection where 161 * the pubsub requests require a specific to address for packets. 162 * 163 * @param connection The XMPP connection 164 * @param toAddress The pubsub specific to address (required for some servers) 165 */ 166 PubSubManager(XMPPConnection connection, BareJid toAddress) { 167 super(connection); 168 pubSubService = toAddress; 169 } 170 171 private void checkIfXmppErrorBecauseOfNotLeafNode(String nodeId, XMPPErrorException xmppErrorException) 172 throws XMPPErrorException, NotALeafNodeException { 173 Condition condition = xmppErrorException.getStanzaError().getCondition(); 174 if (condition == Condition.feature_not_implemented) { 175 // XEP-0060 § 6.5.9.5: Item retrieval not supported, e.g. because node is a collection node 176 throw new PubSubException.NotALeafNodeException(nodeId, pubSubService); 177 } 178 179 throw xmppErrorException; 180 } 181 182 /** 183 * Creates an instant node, if supported. 184 * 185 * @return The node that was created 186 * @throws XMPPErrorException if there was an XMPP error returned. 187 * @throws NoResponseException if there was no response from the remote entity. 188 * @throws NotConnectedException if the XMPP connection is not connected. 189 * @throws InterruptedException if the calling thread was interrupted. 190 */ 191 public LeafNode createNode() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 192 PubSub reply = sendPubsubPacket(IQ.Type.set, new NodeExtension(PubSubElementType.CREATE), null); 193 QName qname = new QName(PubSubNamespace.basic.getXmlns(), "create"); 194 NodeExtension elem = (NodeExtension) reply.getExtension(qname); 195 196 LeafNode newNode = new LeafNode(this, elem.getNode()); 197 nodeMap.put(newNode.getId(), newNode); 198 199 return newNode; 200 } 201 202 /** 203 * Creates a node with default configuration. 204 * 205 * @param nodeId The id of the node, which must be unique within the 206 * pubsub service 207 * @return The node that was created 208 * @throws XMPPErrorException if there was an XMPP error returned. 209 * @throws NoResponseException if there was no response from the remote entity. 210 * @throws NotConnectedException if the XMPP connection is not connected. 211 * @throws InterruptedException if the calling thread was interrupted. 212 */ 213 public LeafNode createNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 214 return (LeafNode) createNode(nodeId, null); 215 } 216 217 /** 218 * Creates a node with specified configuration. 219 * 220 * Note: This is the only way to create a collection node. 221 * 222 * @param nodeId The name of the node, which must be unique within the 223 * pubsub service 224 * @param config The configuration for the node 225 * @return The node that was created 226 * @throws XMPPErrorException if there was an XMPP error returned. 227 * @throws NoResponseException if there was no response from the remote entity. 228 * @throws NotConnectedException if the XMPP connection is not connected. 229 * @throws InterruptedException if the calling thread was interrupted. 230 */ 231 public Node createNode(String nodeId, FillableConfigureForm config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 232 PubSub request = PubSub.createPubsubPacket(pubSubService, IQ.Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId)); 233 boolean isLeafNode = true; 234 235 if (config != null) { 236 DataForm submitForm = config.getDataFormToSubmit(); 237 request.addExtension(new FormNode(FormNodeType.CONFIGURE, submitForm)); 238 NodeType nodeType = config.getNodeType(); 239 // Note that some implementations do to have the pubsub#node_type field in their default configuration, 240 // which I believe to be a bug. However, since PubSub specifies the default node type to be 'leaf' we assume 241 // leaf if the field does not exist. 242 isLeafNode = nodeType == null || nodeType == NodeType.leaf; 243 } 244 245 // Errors will cause exceptions in getReply, so it only returns 246 // on success. 247 sendPubsubPacket(request); 248 Node newNode = isLeafNode ? new LeafNode(this, nodeId) : new CollectionNode(this, nodeId); 249 nodeMap.put(newNode.getId(), newNode); 250 251 return newNode; 252 } 253 254 /** 255 * Retrieves the requested node, if it exists. It will throw an 256 * exception if it does not. 257 * 258 * @param id - The unique id of the node 259 * 260 * @return the node 261 * @throws XMPPErrorException The node does not exist 262 * @throws NoResponseException if there was no response from the server. 263 * @throws NotConnectedException if the XMPP connection is not connected. 264 * @throws InterruptedException if the calling thread was interrupted. 265 * @throws NotAPubSubNodeException if a involved node is not a PubSub node. 266 */ 267 public Node getNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAPubSubNodeException { 268 StringUtils.requireNotNullNorEmpty(id, "The node ID can not be null or the empty string"); 269 Node node = nodeMap.get(id); 270 271 if (node == null) { 272 XMPPConnection connection = connection(); 273 DiscoverInfo info = DiscoverInfo.builder(connection) 274 .to(pubSubService) 275 .setNode(id) 276 .build(); 277 278 DiscoverInfo infoReply = connection.sendIqRequestAndWaitForResponse(info); 279 280 if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) { 281 node = new LeafNode(this, id); 282 } 283 else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) { 284 node = new CollectionNode(this, id); 285 } 286 else { 287 throw new PubSubException.NotAPubSubNodeException(id, infoReply); 288 } 289 nodeMap.put(id, node); 290 } 291 return node; 292 } 293 294 /** 295 * Try to get a leaf node and create one if it does not already exist. 296 * 297 * @param id The unique ID of the node. 298 * @return the leaf node. 299 * @throws NoResponseException if there was no response from the remote entity. 300 * @throws NotConnectedException if the XMPP connection is not connected. 301 * @throws InterruptedException if the calling thread was interrupted. 302 * @throws XMPPErrorException if there was an XMPP error returned. 303 * @throws NotALeafNodeException in case the node already exists as collection node. 304 * @since 4.2.1 305 */ 306 public LeafNode getOrCreateLeafNode(final String id) 307 throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, NotALeafNodeException { 308 try { 309 return getLeafNode(id); 310 } 311 catch (NotAPubSubNodeException e) { 312 return createNode(id); 313 } 314 catch (XMPPErrorException e1) { 315 if (e1.getStanzaError().getCondition() == Condition.item_not_found) { 316 try { 317 return createNode(id); 318 } 319 catch (XMPPErrorException e2) { 320 if (e2.getStanzaError().getCondition() == Condition.conflict) { 321 // The node was created in the meantime, re-try getNode(). Note that this case should be rare. 322 try { 323 return getLeafNode(id); 324 } 325 catch (NotAPubSubNodeException e) { 326 // Should not happen 327 throw new IllegalStateException(e); 328 } 329 } 330 throw e2; 331 } 332 } 333 if (e1.getStanzaError().getCondition() == Condition.service_unavailable) { 334 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not 335 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or 336 // collection node. 337 LOGGER.warning("The PubSub service " + pubSubService 338 + " threw an DiscoInfoNodeAssertionError, trying workaround for Prosody bug #805 (https://prosody.im/issues/issue/805)"); 339 return getOrCreateLeafNodeProsodyWorkaround(id); 340 } 341 throw e1; 342 } 343 } 344 345 /** 346 * Try to get a leaf node with the given node ID. 347 * 348 * @param id the node ID. 349 * @return the requested leaf node. 350 * @throws NotALeafNodeException in case the node exists but is a collection node. 351 * @throws NoResponseException if there was no response from the remote entity. 352 * @throws NotConnectedException if the XMPP connection is not connected. 353 * @throws InterruptedException if the calling thread was interrupted. 354 * @throws XMPPErrorException if there was an XMPP error returned. 355 * @throws NotAPubSubNodeException if a involved node is not a PubSub node. 356 * @since 4.2.1 357 */ 358 public LeafNode getLeafNode(String id) throws NotALeafNodeException, NoResponseException, NotConnectedException, 359 InterruptedException, XMPPErrorException, NotAPubSubNodeException { 360 Node node; 361 try { 362 node = getNode(id); 363 } 364 catch (XMPPErrorException e) { 365 if (e.getStanzaError().getCondition() == Condition.service_unavailable) { 366 // This could be caused by Prosody bug #805 (see https://prosody.im/issues/issue/805). Prosody does not 367 // answer to disco#info requests on the node ID, which makes it undecidable if a node is a leaf or 368 // collection node. 369 return getLeafNodeProsodyWorkaround(id); 370 } 371 throw e; 372 } 373 374 if (node instanceof LeafNode) { 375 return (LeafNode) node; 376 } 377 378 throw new PubSubException.NotALeafNodeException(id, pubSubService); 379 } 380 381 private LeafNode getLeafNodeProsodyWorkaround(final String id) throws NoResponseException, NotConnectedException, 382 InterruptedException, NotALeafNodeException, XMPPErrorException { 383 LeafNode leafNode = new LeafNode(this, id); 384 try { 385 // Try to ensure that this is not a collection node by asking for one item form the node. 386 leafNode.getItems(1); 387 } catch (XMPPErrorException e) { 388 checkIfXmppErrorBecauseOfNotLeafNode(id, e); 389 } 390 391 nodeMap.put(id, leafNode); 392 393 return leafNode; 394 } 395 396 private LeafNode getOrCreateLeafNodeProsodyWorkaround(final String id) 397 throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotALeafNodeException { 398 try { 399 return createNode(id); 400 } 401 catch (XMPPErrorException e1) { 402 if (e1.getStanzaError().getCondition() == Condition.conflict) { 403 return getLeafNodeProsodyWorkaround(id); 404 } 405 throw e1; 406 } 407 } 408 409 /** 410 * Try to publish an item and, if the node with the given ID does not exists, auto-create the node. 411 * <p> 412 * Not every PubSub service supports automatic node creation. You can discover if this service supports it by using 413 * {@link #supportsAutomaticNodeCreation()}. 414 * </p> 415 * 416 * @param id The unique id of the node. 417 * @param item The item to publish. 418 * @param <I> type of the item. 419 * 420 * @return the LeafNode on which the item was published. 421 * @throws NoResponseException if there was no response from the remote entity. 422 * @throws XMPPErrorException if there was an XMPP error returned. 423 * @throws NotConnectedException if the XMPP connection is not connected. 424 * @throws InterruptedException if the calling thread was interrupted. 425 * @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. 426 * @since 4.2.1 427 */ 428 public <I extends Item> LeafNode tryToPublishAndPossibleAutoCreate(String id, I item) 429 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, 430 NotALeafNodeException { 431 LeafNode leafNode = new LeafNode(this, id); 432 433 try { 434 leafNode.publish(item); 435 } catch (XMPPErrorException e) { 436 checkIfXmppErrorBecauseOfNotLeafNode(id, e); 437 } 438 439 // If LeafNode.publish() did not throw then we have successfully published an item and possible auto-created 440 // (XEP-0163 § 3., XEP-0060 § 7.1.4) the node. So we can put the node into the nodeMap. 441 nodeMap.put(id, leafNode); 442 443 return leafNode; 444 } 445 446 /** 447 * Get all the nodes that currently exist as a child of the specified 448 * collection node. If the service does not support collection nodes 449 * then all nodes will be returned. 450 * 451 * To retrieve contents of the root collection node (if it exists), 452 * or there is no root collection node, pass null as the nodeId. 453 * 454 * @param nodeId - The id of the collection node for which the child 455 * nodes will be returned. 456 * @return {@link DiscoverItems} representing the existing nodes 457 * @throws XMPPErrorException if there was an XMPP error returned. 458 * @throws NoResponseException if there was no response from the server. 459 * @throws NotConnectedException if the XMPP connection is not connected. 460 * @throws InterruptedException if the calling thread was interrupted. 461 */ 462 public DiscoverItems discoverNodes(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 463 DiscoverItems items = new DiscoverItems(); 464 465 if (nodeId != null) 466 items.setNode(nodeId); 467 items.setTo(pubSubService); 468 DiscoverItems nodeItems = connection().sendIqRequestAndWaitForResponse(items); 469 return nodeItems; 470 } 471 472 /** 473 * Gets the subscriptions on the root node. 474 * 475 * @return List of exceptions 476 * @throws XMPPErrorException if there was an XMPP error returned. 477 * @throws NoResponseException if there was no response from the remote entity. 478 * @throws NotConnectedException if the XMPP connection is not connected. 479 * @throws InterruptedException if the calling thread was interrupted. 480 */ 481 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 482 Stanza reply = sendPubsubPacket(IQ.Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS), null); 483 SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtensionElement(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns()); 484 return subElem.getSubscriptions(); 485 } 486 487 /** 488 * Gets the affiliations on the root node. 489 * 490 * @return List of affiliations 491 * @throws XMPPErrorException if there was an XMPP error returned. 492 * @throws NoResponseException if there was no response from the remote entity. 493 * @throws NotConnectedException if the XMPP connection is not connected. 494 * @throws InterruptedException if the calling thread was interrupted. 495 * 496 */ 497 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 498 PubSub reply = sendPubsubPacket(IQ.Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS), null); 499 AffiliationsExtension listElem = reply.getExtension(PubSubElementType.AFFILIATIONS); 500 return listElem.getAffiliations(); 501 } 502 503 /** 504 * Delete the specified node. 505 * 506 * @param nodeId TODO javadoc me please 507 * @throws XMPPErrorException if there was an XMPP error returned. 508 * @throws NoResponseException if there was no response from the remote entity. 509 * @throws NotConnectedException if the XMPP connection is not connected. 510 * @throws InterruptedException if the calling thread was interrupted. 511 * @return <code>true</code> if this node existed and was deleted and <code>false</code> if this node did not exist. 512 */ 513 public boolean deleteNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 514 boolean res = true; 515 try { 516 sendPubsubPacket(IQ.Type.set, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace()); 517 } catch (XMPPErrorException e) { 518 if (e.getStanzaError().getCondition() == StanzaError.Condition.item_not_found) { 519 res = false; 520 } else { 521 throw e; 522 } 523 } 524 nodeMap.remove(nodeId); 525 return res; 526 } 527 528 /** 529 * Returns the default settings for Node configuration. 530 * 531 * @return configuration form containing the default settings. 532 * @throws XMPPErrorException if there was an XMPP error returned. 533 * @throws NoResponseException if there was no response from the remote entity. 534 * @throws NotConnectedException if the XMPP connection is not connected. 535 * @throws InterruptedException if the calling thread was interrupted. 536 */ 537 public ConfigureForm getDefaultConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 538 // Errors will cause exceptions in getReply, so it only returns 539 // on success. 540 PubSub reply = sendPubsubPacket(IQ.Type.get, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace()); 541 return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT); 542 } 543 544 /** 545 * Get the JID of the PubSub service managed by this manager. 546 * 547 * @return the JID of the PubSub service. 548 */ 549 public BareJid getServiceJid() { 550 return pubSubService; 551 } 552 553 /** 554 * Gets the supported features of the servers pubsub implementation 555 * as a standard {@link DiscoverInfo} instance. 556 * 557 * @return The supported features 558 * @throws XMPPErrorException if there was an XMPP error returned. 559 * @throws NoResponseException if there was no response from the remote entity. 560 * @throws NotConnectedException if the XMPP connection is not connected. 561 * @throws InterruptedException if the calling thread was interrupted. 562 */ 563 public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 564 ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(connection()); 565 return mgr.discoverInfo(pubSubService); 566 } 567 568 /** 569 * Check if the PubSub service supports automatic node creation. 570 * 571 * @return true if the PubSub service supports automatic node creation. 572 * @throws NoResponseException if there was no response from the remote entity. 573 * @throws XMPPErrorException if there was an XMPP error returned. 574 * @throws NotConnectedException if the XMPP connection is not connected. 575 * @throws InterruptedException if the calling thread was interrupted. 576 * @since 4.2.1 577 * @see <a href="https://xmpp.org/extensions/xep-0060.html#publisher-publish-autocreate">XEP-0060 § 7.1.4 Automatic Node Creation</a> 578 */ 579 public boolean supportsAutomaticNodeCreation() 580 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 581 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection()); 582 return sdm.supportsFeature(pubSubService, AUTO_CREATE_FEATURE); 583 } 584 585 /** 586 * Check if it is possible to create PubSub nodes on this service. It could be possible that the 587 * PubSub service allows only certain XMPP entities (clients) to create nodes and publish items 588 * to them. 589 * <p> 590 * Note that since XEP-60 does not provide an API to determine if an XMPP entity is allowed to 591 * create nodes, therefore this method creates an instant node calling {@link #createNode()} to 592 * determine if it is possible to create nodes. 593 * </p> 594 * 595 * @return <code>true</code> if it is possible to create nodes, <code>false</code> otherwise. 596 * @throws NoResponseException if there was no response from the remote entity. 597 * @throws NotConnectedException if the XMPP connection is not connected. 598 * @throws InterruptedException if the calling thread was interrupted. 599 * @throws XMPPErrorException if there was an XMPP error returned. 600 */ 601 public boolean canCreateNodesAndPublishItems() throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException { 602 LeafNode leafNode = null; 603 try { 604 leafNode = createNode(); 605 } 606 catch (XMPPErrorException e) { 607 if (e.getStanzaError().getCondition() == StanzaError.Condition.forbidden) { 608 return false; 609 } 610 throw e; 611 } finally { 612 if (leafNode != null) { 613 deleteNode(leafNode.getId()); 614 } 615 } 616 return true; 617 } 618 619 private PubSub sendPubsubPacket(IQ.Type type, XmlElement ext, PubSubNamespace ns) 620 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 621 return sendPubsubPacket(pubSubService, type, Collections.singletonList(ext), ns); 622 } 623 624 XMPPConnection getConnection() { 625 return connection(); 626 } 627 628 PubSub sendPubsubPacket(Jid to, IQ.Type type, List<XmlElement> extList, PubSubNamespace ns) 629 throws NoResponseException, XMPPErrorException, NotConnectedException, 630 InterruptedException { 631// CHECKSTYLE:OFF 632 PubSub pubSub = new PubSub(to, type, ns); 633 for (XmlElement pe : extList) { 634 pubSub.addExtension(pe); 635 } 636// CHECKSTYLE:ON 637 return sendPubsubPacket(pubSub); 638 } 639 640 PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, 641 NotConnectedException, InterruptedException { 642 IQ resultIQ = connection().sendIqRequestAndWaitForResponse(packet); 643 if (resultIQ instanceof EmptyResultIQ) { 644 return null; 645 } 646 return (PubSub) resultIQ; 647 } 648 649 /** 650 * Get the "default" PubSub service for a given XMPP connection. The default PubSub service is 651 * simply an arbitrary XMPP service with the PubSub feature and an identity of category "pubsub" 652 * and type "service". 653 * 654 * @param connection TODO javadoc me please 655 * @return the default PubSub service or <code>null</code>. 656 * @throws NoResponseException if there was no response from the remote entity. 657 * @throws XMPPErrorException if there was an XMPP error returned. 658 * @throws NotConnectedException if the XMPP connection is not connected. 659 * @throws InterruptedException if the calling thread was interrupted. 660 * @see <a href="http://xmpp.org/extensions/xep-0060.html#entity-features">XEP-60 § 5.1 Discover 661 * Features</a> 662 */ 663 public static DomainBareJid getPubSubService(XMPPConnection connection) 664 throws NoResponseException, XMPPErrorException, NotConnectedException, 665 InterruptedException { 666 return ServiceDiscoveryManager.getInstanceFor(connection).findService(PubSub.NAMESPACE, 667 true, "pubsub", "service"); 668 } 669}