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}