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.List; 021import java.util.Map; 022import java.util.concurrent.ConcurrentHashMap; 023 024import org.jivesoftware.smack.SmackException.NoResponseException; 025import org.jivesoftware.smack.SmackException.NotConnectedException; 026import org.jivesoftware.smack.XMPPConnection; 027import org.jivesoftware.smack.XMPPException.XMPPErrorException; 028import org.jivesoftware.smack.packet.EmptyResultIQ; 029import org.jivesoftware.smack.packet.IQ; 030import org.jivesoftware.smack.packet.IQ.Type; 031import org.jivesoftware.smack.packet.Stanza; 032import org.jivesoftware.smack.packet.ExtensionElement; 033import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 034import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 035import org.jivesoftware.smackx.disco.packet.DiscoverItems; 036import org.jivesoftware.smackx.pubsub.packet.PubSub; 037import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; 038import org.jivesoftware.smackx.pubsub.util.NodeUtils; 039import org.jivesoftware.smackx.xdata.Form; 040import org.jivesoftware.smackx.xdata.FormField; 041 042/** 043 * This is the starting point for access to the pubsub service. It 044 * will provide access to general information about the service, as 045 * well as create or retrieve pubsub {@link LeafNode} instances. These 046 * instances provide the bulk of the functionality as defined in the 047 * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>. 048 * 049 * @author Robin Collier 050 */ 051final public class PubSubManager 052{ 053 private XMPPConnection con; 054 private String to; 055 private Map<String, Node> nodeMap = new ConcurrentHashMap<String, Node>(); 056 057 /** 058 * Create a pubsub manager associated to the specified connection. Defaults the service 059 * name to <i>pubsub</i> 060 * 061 * @param connection The XMPP connection 062 */ 063 public PubSubManager(XMPPConnection connection) 064 { 065 con = connection; 066 to = "pubsub." + connection.getServiceName(); 067 } 068 069 /** 070 * Create a pubsub manager associated to the specified connection where 071 * the pubsub requests require a specific to address for packets. 072 * 073 * @param connection The XMPP connection 074 * @param toAddress The pubsub specific to address (required for some servers) 075 */ 076 public PubSubManager(XMPPConnection connection, String toAddress) 077 { 078 con = connection; 079 to = toAddress; 080 } 081 082 /** 083 * Creates an instant node, if supported. 084 * 085 * @return The node that was created 086 * @throws XMPPErrorException 087 * @throws NoResponseException 088 * @throws NotConnectedException 089 */ 090 public LeafNode createNode() throws NoResponseException, XMPPErrorException, NotConnectedException 091 { 092 PubSub reply = sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.CREATE), null); 093 NodeExtension elem = reply.getExtension("create", PubSubNamespace.BASIC.getXmlns()); 094 095 LeafNode newNode = new LeafNode(con, elem.getNode()); 096 newNode.setTo(to); 097 nodeMap.put(newNode.getId(), newNode); 098 099 return newNode; 100 } 101 102 /** 103 * Creates a node with default configuration. 104 * 105 * @param id The id of the node, which must be unique within the 106 * pubsub service 107 * @return The node that was created 108 * @throws XMPPErrorException 109 * @throws NoResponseException 110 * @throws NotConnectedException 111 */ 112 public LeafNode createNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException 113 { 114 return (LeafNode)createNode(id, null); 115 } 116 117 /** 118 * Creates a node with specified configuration. 119 * 120 * Note: This is the only way to create a collection node. 121 * 122 * @param name The name of the node, which must be unique within the 123 * pubsub service 124 * @param config The configuration for the node 125 * @return The node that was created 126 * @throws XMPPErrorException 127 * @throws NoResponseException 128 * @throws NotConnectedException 129 */ 130 public Node createNode(String name, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException 131 { 132 PubSub request = PubSub.createPubsubPacket(to, Type.set, new NodeExtension(PubSubElementType.CREATE, name), null); 133 boolean isLeafNode = true; 134 135 if (config != null) 136 { 137 request.addExtension(new FormNode(FormNodeType.CONFIGURE, config)); 138 FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName()); 139 140 if (nodeTypeField != null) 141 isLeafNode = nodeTypeField.getValues().get(0).equals(NodeType.leaf.toString()); 142 } 143 144 // Errors will cause exceptions in getReply, so it only returns 145 // on success. 146 sendPubsubPacket(con, request); 147 Node newNode = isLeafNode ? new LeafNode(con, name) : new CollectionNode(con, name); 148 newNode.setTo(to); 149 nodeMap.put(newNode.getId(), newNode); 150 151 return newNode; 152 } 153 154 /** 155 * Retrieves the requested node, if it exists. It will throw an 156 * exception if it does not. 157 * 158 * @param id - The unique id of the node 159 * @return the node 160 * @throws XMPPErrorException The node does not exist 161 * @throws NoResponseException if there was no response from the server. 162 * @throws NotConnectedException 163 */ 164 @SuppressWarnings("unchecked") 165 public <T extends Node> T getNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException 166 { 167 Node node = nodeMap.get(id); 168 169 if (node == null) 170 { 171 DiscoverInfo info = new DiscoverInfo(); 172 info.setTo(to); 173 info.setNode(id); 174 175 DiscoverInfo infoReply = (DiscoverInfo) con.createPacketCollectorAndSend(info).nextResultOrThrow(); 176 177 if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) { 178 node = new LeafNode(con, id); 179 } 180 else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) { 181 node = new CollectionNode(con, id); 182 } 183 else { 184 // XEP-60 5.3 states that 185 // "The 'disco#info' result MUST include an identity with a category of 'pubsub' and a type of either 'leaf' or 'collection'." 186 // If this is not the case, then we are dealing with an PubSub implementation that doesn't follow the specification. 187 throw new AssertionError( 188 "PubSub service '" 189 + to 190 + "' returned disco info result for node '" 191 + id 192 + "', 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."); 193 } 194 node.setTo(to); 195 nodeMap.put(id, node); 196 } 197 return (T) node; 198 } 199 200 /** 201 * Get all the nodes that currently exist as a child of the specified 202 * collection node. If the service does not support collection nodes 203 * then all nodes will be returned. 204 * 205 * To retrieve contents of the root collection node (if it exists), 206 * or there is no root collection node, pass null as the nodeId. 207 * 208 * @param nodeId - The id of the collection node for which the child 209 * nodes will be returned. 210 * @return {@link DiscoverItems} representing the existing nodes 211 * @throws XMPPErrorException 212 * @throws NoResponseException if there was no response from the server. 213 * @throws NotConnectedException 214 */ 215 public DiscoverItems discoverNodes(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException 216 { 217 DiscoverItems items = new DiscoverItems(); 218 219 if (nodeId != null) 220 items.setNode(nodeId); 221 items.setTo(to); 222 DiscoverItems nodeItems = (DiscoverItems) con.createPacketCollectorAndSend(items).nextResultOrThrow(); 223 return nodeItems; 224 } 225 226 /** 227 * Gets the subscriptions on the root node. 228 * 229 * @return List of exceptions 230 * @throws XMPPErrorException 231 * @throws NoResponseException 232 * @throws NotConnectedException 233 */ 234 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException 235 { 236 Stanza reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS), null); 237 SubscriptionsExtension subElem = reply.getExtension(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns()); 238 return subElem.getSubscriptions(); 239 } 240 241 /** 242 * Gets the affiliations on the root node. 243 * 244 * @return List of affiliations 245 * @throws XMPPErrorException 246 * @throws NoResponseException 247 * @throws NotConnectedException 248 * 249 */ 250 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, NotConnectedException 251 { 252 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS), null); 253 AffiliationsExtension listElem = reply.getExtension(PubSubElementType.AFFILIATIONS); 254 return listElem.getAffiliations(); 255 } 256 257 /** 258 * Delete the specified node 259 * 260 * @param nodeId 261 * @throws XMPPErrorException 262 * @throws NoResponseException 263 * @throws NotConnectedException 264 */ 265 public void deleteNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException 266 { 267 sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace()); 268 nodeMap.remove(nodeId); 269 } 270 271 /** 272 * Returns the default settings for Node configuration. 273 * 274 * @return configuration form containing the default settings. 275 * @throws XMPPErrorException 276 * @throws NoResponseException 277 * @throws NotConnectedException 278 */ 279 public ConfigureForm getDefaultConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException 280 { 281 // Errors will cause exceptions in getReply, so it only returns 282 // on success. 283 PubSub reply = sendPubsubPacket(Type.get, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace()); 284 return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT); 285 } 286 287 /** 288 * Gets the supported features of the servers pubsub implementation 289 * as a standard {@link DiscoverInfo} instance. 290 * 291 * @return The supported features 292 * @throws XMPPErrorException 293 * @throws NoResponseException 294 * @throws NotConnectedException 295 */ 296 public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException 297 { 298 ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(con); 299 return mgr.discoverInfo(to); 300 } 301 302 private PubSub sendPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 303 throws NoResponseException, XMPPErrorException, NotConnectedException { 304 return sendPubsubPacket(con, to, type, Collections.singletonList(ext), ns); 305 } 306 307 static PubSub sendPubsubPacket(XMPPConnection con, String to, Type type, List<ExtensionElement> extList, PubSubNamespace ns) throws NoResponseException, XMPPErrorException, NotConnectedException 308 { 309 PubSub pubSub = new PubSub(to, type, ns); 310 for (ExtensionElement pe : extList) { 311 pubSub.addExtension(pe); 312 } 313 return sendPubsubPacket(con ,pubSub); 314 } 315 316 static PubSub sendPubsubPacket(XMPPConnection con, PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException 317 { 318 IQ resultIQ = con.createPacketCollectorAndSend(packet).nextResultOrThrow(); 319 if (resultIQ instanceof EmptyResultIQ) { 320 return null; 321 } 322 return (PubSub) resultIQ; 323 } 324 325}