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.IQ; 035import org.jivesoftware.smack.packet.XMPPError; 036import org.jivesoftware.smack.packet.IQ.Type; 037import org.jivesoftware.smack.packet.Stanza; 038import org.jivesoftware.smack.packet.ExtensionElement; 039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 040import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 041import org.jivesoftware.smackx.disco.packet.DiscoverItems; 042import org.jivesoftware.smackx.pubsub.packet.PubSub; 043import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; 044import org.jivesoftware.smackx.pubsub.util.NodeUtils; 045import org.jivesoftware.smackx.xdata.Form; 046import org.jivesoftware.smackx.xdata.FormField; 047import org.jxmpp.jid.BareJid; 048import org.jxmpp.jid.DomainBareJid; 049import org.jxmpp.jid.Jid; 050import org.jxmpp.jid.impl.JidCreate; 051import org.jxmpp.stringprep.XmppStringprepException; 052 053/** 054 * This is the starting point for access to the pubsub service. It 055 * will provide access to general information about the service, as 056 * well as create or retrieve pubsub {@link LeafNode} instances. These 057 * instances provide the bulk of the functionality as defined in the 058 * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>. 059 * 060 * @author Robin Collier 061 */ 062public final class PubSubManager extends Manager { 063 064 private static final Logger LOGGER = Logger.getLogger(PubSubManager.class.getName()); 065 private static final Map<XMPPConnection, Map<BareJid, PubSubManager>> INSTANCES = new WeakHashMap<>(); 066 067 /** 068 * The JID of the PubSub service this manager manages. 069 */ 070 private final BareJid pubSubService; 071 072 /** 073 * A map of node IDs to Nodes, used to cache those Nodes. This does only cache the type of Node, 074 * i.e. {@link CollectionNode} or {@link LeafNode}. 075 */ 076 private final Map<String, Node> nodeMap = new ConcurrentHashMap<String, Node>(); 077 078 /** 079 * Get a PubSub manager for the default PubSub service of the connection. 080 * 081 * @param connection 082 * @return the default PubSub manager. 083 */ 084 public static PubSubManager getInstance(XMPPConnection connection) { 085 DomainBareJid pubSubService = null; 086 if (connection.isAuthenticated()) { 087 try { 088 pubSubService = getPubSubService(connection); 089 } 090 catch (NoResponseException | XMPPErrorException | NotConnectedException e) { 091 LOGGER.log(Level.WARNING, "Could not determine PubSub service", e); 092 } 093 catch (InterruptedException e) { 094 LOGGER.log(Level.FINE, "Interupted while trying to determine PubSub service", e); 095 } 096 } 097 if (pubSubService == null) { 098 try { 099 // Perform an educated guess about what the PubSub service's domain bare JID may be 100 pubSubService = JidCreate.domainBareFrom("pubsub." + connection.getXMPPServiceDomain()); 101 } 102 catch (XmppStringprepException e) { 103 throw new RuntimeException(e); 104 } 105 } 106 return getInstance(connection, pubSubService); 107 } 108 109 /** 110 * Get the PubSub manager for the given connection and PubSub service. 111 * 112 * @param connection the XMPP connection. 113 * @param pubSubService the PubSub service. 114 * @return a PubSub manager for the connection and service. 115 */ 116 public static synchronized PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) { 117 Map<BareJid, PubSubManager> managers = INSTANCES.get(connection); 118 if (managers == null) { 119 managers = new HashMap<>(); 120 INSTANCES.put(connection, managers); 121 } 122 PubSubManager pubSubManager = managers.get(pubSubService); 123 if (pubSubManager == null) { 124 pubSubManager = new PubSubManager(connection, pubSubService); 125 managers.put(pubSubService, pubSubManager); 126 } 127 return pubSubManager; 128 } 129 130 /** 131 * Create a pubsub manager associated to the specified connection where 132 * the pubsub requests require a specific to address for packets. 133 * 134 * @param connection The XMPP connection 135 * @param toAddress The pubsub specific to address (required for some servers) 136 */ 137 PubSubManager(XMPPConnection connection, BareJid toAddress) 138 { 139 super(connection); 140 pubSubService = toAddress; 141 } 142 143 /** 144 * Creates an instant node, if supported. 145 * 146 * @return The node that was created 147 * @throws XMPPErrorException 148 * @throws NoResponseException 149 * @throws NotConnectedException 150 * @throws InterruptedException 151 */ 152 public LeafNode createNode() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 153 { 154 PubSub reply = sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.CREATE), null); 155 NodeExtension elem = reply.getExtension("create", PubSubNamespace.BASIC.getXmlns()); 156 157 LeafNode newNode = new LeafNode(this, elem.getNode()); 158 nodeMap.put(newNode.getId(), newNode); 159 160 return newNode; 161 } 162 163 /** 164 * Creates a node with default configuration. 165 * 166 * @param nodeId The id of the node, which must be unique within the 167 * pubsub service 168 * @return The node that was created 169 * @throws XMPPErrorException 170 * @throws NoResponseException 171 * @throws NotConnectedException 172 * @throws InterruptedException 173 */ 174 public LeafNode createNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 175 { 176 return (LeafNode) createNode(nodeId, null); 177 } 178 179 /** 180 * Creates a node with specified configuration. 181 * 182 * Note: This is the only way to create a collection node. 183 * 184 * @param nodeId The name of the node, which must be unique within the 185 * pubsub service 186 * @param config The configuration for the node 187 * @return The node that was created 188 * @throws XMPPErrorException 189 * @throws NoResponseException 190 * @throws NotConnectedException 191 * @throws InterruptedException 192 */ 193 public Node createNode(String nodeId, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 194 { 195 PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId), null); 196 boolean isLeafNode = true; 197 198 if (config != null) 199 { 200 request.addExtension(new FormNode(FormNodeType.CONFIGURE, config)); 201 FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName()); 202 203 if (nodeTypeField != null) 204 isLeafNode = nodeTypeField.getValues().get(0).equals(NodeType.leaf.toString()); 205 } 206 207 // Errors will cause exceptions in getReply, so it only returns 208 // on success. 209 sendPubsubPacket(request); 210 Node newNode = isLeafNode ? new LeafNode(this, nodeId) : new CollectionNode(this, nodeId); 211 nodeMap.put(newNode.getId(), newNode); 212 213 return newNode; 214 } 215 216 /** 217 * Retrieves the requested node, if it exists. It will throw an 218 * exception if it does not. 219 * 220 * @param id - The unique id of the node 221 * @return the node 222 * @throws XMPPErrorException The node does not exist 223 * @throws NoResponseException if there was no response from the server. 224 * @throws NotConnectedException 225 * @throws InterruptedException 226 */ 227 public <T extends Node> T getNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 228 { 229 Node node = nodeMap.get(id); 230 231 if (node == null) 232 { 233 DiscoverInfo info = new DiscoverInfo(); 234 info.setTo(pubSubService); 235 info.setNode(id); 236 237 DiscoverInfo infoReply = connection().createStanzaCollectorAndSend(info).nextResultOrThrow(); 238 239 if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) { 240 node = new LeafNode(this, id); 241 } 242 else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) { 243 node = new CollectionNode(this, id); 244 } 245 else { 246 // XEP-60 5.3 states that 247 // "The 'disco#info' result MUST include an identity with a category of 'pubsub' and a type of either 'leaf' or 'collection'." 248 // If this is not the case, then we are dealing with an PubSub implementation that doesn't follow the specification. 249 throw new AssertionError( 250 "PubSub service '" 251 + pubSubService 252 + "' returned disco info result for node '" 253 + id 254 + "', but it did not contain an Identity of type 'leaf' or 'collection' (and category 'pubsub'), which is not allowed according to XEP-60 5.3."); 255 } 256 nodeMap.put(id, node); 257 } 258 @SuppressWarnings("unchecked") 259 T res = (T) node; 260 return res; 261 } 262 263 /** 264 * Get all the nodes that currently exist as a child of the specified 265 * collection node. If the service does not support collection nodes 266 * then all nodes will be returned. 267 * 268 * To retrieve contents of the root collection node (if it exists), 269 * or there is no root collection node, pass null as the nodeId. 270 * 271 * @param nodeId - The id of the collection node for which the child 272 * nodes will be returned. 273 * @return {@link DiscoverItems} representing the existing nodes 274 * @throws XMPPErrorException 275 * @throws NoResponseException if there was no response from the server. 276 * @throws NotConnectedException 277 * @throws InterruptedException 278 */ 279 public DiscoverItems discoverNodes(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 280 { 281 DiscoverItems items = new DiscoverItems(); 282 283 if (nodeId != null) 284 items.setNode(nodeId); 285 items.setTo(pubSubService); 286 DiscoverItems nodeItems = connection().createStanzaCollectorAndSend(items).nextResultOrThrow(); 287 return nodeItems; 288 } 289 290 /** 291 * Gets the subscriptions on the root node. 292 * 293 * @return List of exceptions 294 * @throws XMPPErrorException 295 * @throws NoResponseException 296 * @throws NotConnectedException 297 * @throws InterruptedException 298 */ 299 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 300 { 301 Stanza reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS), null); 302 SubscriptionsExtension subElem = reply.getExtension(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns()); 303 return subElem.getSubscriptions(); 304 } 305 306 /** 307 * Gets the affiliations on the root node. 308 * 309 * @return List of affiliations 310 * @throws XMPPErrorException 311 * @throws NoResponseException 312 * @throws NotConnectedException 313 * @throws InterruptedException 314 * 315 */ 316 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 317 { 318 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS), null); 319 AffiliationsExtension listElem = reply.getExtension(PubSubElementType.AFFILIATIONS); 320 return listElem.getAffiliations(); 321 } 322 323 /** 324 * Delete the specified node. 325 * 326 * @param nodeId 327 * @throws XMPPErrorException 328 * @throws NoResponseException 329 * @throws NotConnectedException 330 * @throws InterruptedException 331 */ 332 public void deleteNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 333 { 334 sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace()); 335 nodeMap.remove(nodeId); 336 } 337 338 /** 339 * Returns the default settings for Node configuration. 340 * 341 * @return configuration form containing the default settings. 342 * @throws XMPPErrorException 343 * @throws NoResponseException 344 * @throws NotConnectedException 345 * @throws InterruptedException 346 */ 347 public ConfigureForm getDefaultConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 348 { 349 // Errors will cause exceptions in getReply, so it only returns 350 // on success. 351 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace()); 352 return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT); 353 } 354 355 /** 356 * Get the JID of the PubSub service managed by this manager. 357 * 358 * @return the JID of the PubSub service. 359 */ 360 public BareJid getServiceJid() { 361 return pubSubService; 362 } 363 364 /** 365 * Gets the supported features of the servers pubsub implementation 366 * as a standard {@link DiscoverInfo} instance. 367 * 368 * @return The supported features 369 * @throws XMPPErrorException 370 * @throws NoResponseException 371 * @throws NotConnectedException 372 * @throws InterruptedException 373 */ 374 public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 375 { 376 ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(connection()); 377 return mgr.discoverInfo(pubSubService); 378 } 379 380 /** 381 * Check if it is possible to create PubSub nodes on this service. It could be possible that the 382 * PubSub service allows only certain XMPP entities (clients) to create nodes and publish items 383 * to them. 384 * <p> 385 * Note that since XEP-60 does not provide an API to determine if an XMPP entity is allowed to 386 * create nodes, therefore this method creates an instant node calling {@link #createNode()} to 387 * determine if it is possible to create nodes. 388 * </p> 389 * 390 * @return <code>true</code> if it is possible to create nodes, <code>false</code> otherwise. 391 * @throws NoResponseException 392 * @throws NotConnectedException 393 * @throws InterruptedException 394 * @throws XMPPErrorException 395 */ 396 public boolean canCreateNodesAndPublishItems() throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException { 397 LeafNode leafNode = null; 398 try { 399 leafNode = createNode(); 400 } 401 catch (XMPPErrorException e) { 402 if (e.getXMPPError().getCondition() == XMPPError.Condition.forbidden) { 403 return false; 404 } 405 throw e; 406 } finally { 407 if (leafNode != null) { 408 deleteNode(leafNode.getId()); 409 } 410 } 411 return true; 412 } 413 414 private PubSub sendPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 415 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 416 return sendPubsubPacket(pubSubService, type, Collections.singletonList(ext), ns); 417 } 418 419 XMPPConnection getConnection() { 420 return connection(); 421 } 422 423 PubSub sendPubsubPacket(Jid to, Type type, List<ExtensionElement> extList, PubSubNamespace ns) 424 throws NoResponseException, XMPPErrorException, NotConnectedException, 425 InterruptedException { 426// CHECKSTYLE:OFF 427 PubSub pubSub = new PubSub(to, type, ns); 428 for (ExtensionElement pe : extList) { 429 pubSub.addExtension(pe); 430 } 431// CHECKSTYLE:ON 432 return sendPubsubPacket(pubSub); 433 } 434 435 PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, 436 NotConnectedException, InterruptedException { 437 IQ resultIQ = connection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); 438 if (resultIQ instanceof EmptyResultIQ) { 439 return null; 440 } 441 return (PubSub) resultIQ; 442 } 443 444 /** 445 * Get the "default" PubSub service for a given XMPP connection. The default PubSub service is 446 * simply an arbitrary XMPP service with the PubSub feature and an identity of category "pubsub" 447 * and type "service". 448 * 449 * @param connection 450 * @return the default PubSub service or <code>null</code>. 451 * @throws NoResponseException 452 * @throws XMPPErrorException 453 * @throws NotConnectedException 454 * @throws InterruptedException 455 * @see <a href="http://xmpp.org/extensions/xep-0060.html#entity-features">XEP-60 ยง 5.1 Discover 456 * Features</a> 457 */ 458 public static DomainBareJid getPubSubService(XMPPConnection connection) 459 throws NoResponseException, XMPPErrorException, NotConnectedException, 460 InterruptedException { 461 return ServiceDiscoveryManager.getInstanceFor(connection).findService(PubSub.NAMESPACE, 462 true, "pubsub", "service"); 463 } 464}