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