diff --git a/documentation/extensions/pubsub.md b/documentation/extensions/pubsub.md index 788f63d18..1235ad5c1 100644 --- a/documentation/extensions/pubsub.md +++ b/documentation/extensions/pubsub.md @@ -53,7 +53,7 @@ Create a node with default configuration and then configure it: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Create the node LeafNode leaf = mgr.createNode("testNode"); @@ -71,7 +71,7 @@ Create and configure a node: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Create the node ConfigureForm form = new ConfigureForm(FormType.submit); @@ -108,7 +108,7 @@ In this example we publish an item to a node that does not take payload: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -124,7 +124,7 @@ In this example we publish an item to a node that does take payload: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -167,7 +167,7 @@ subscribe for messages. ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -198,7 +198,7 @@ subscribe for item deletion messages. ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -230,7 +230,7 @@ subscribe for node configuration messages. ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node Node node = mgr.getNode("testNode"); @@ -286,7 +286,7 @@ In this example we can see how to retrieve the existing items from a node: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -298,7 +298,7 @@ In this example we can see how to retrieve the last N existing items: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -310,7 +310,7 @@ In this example we can see how to retrieve the specified existing items: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the node LeafNode node = mgr.getNode("testNode"); @@ -341,7 +341,7 @@ In this example we can see how to get pubsub capabilities: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the pubsub features that are supported DiscoverInfo supportedFeatures = mgr.getSupportedFeatures(); @@ -351,7 +351,7 @@ In this example we can see how to get pubsub subscriptions for all nodes: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get all the subscriptions in the pubsub service List<Subscription;> subscriptions = mgr.getSubscriptions(); @@ -362,7 +362,7 @@ on the pubsub service: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); // Get the affiliations for the users bare JID List<Affiliation;> affiliations = mgr.getAffiliations(); @@ -372,7 +372,7 @@ In this example we can see how to get information about the node: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); Node node = mgr.getNode("testNode"); // Get the node information @@ -383,7 +383,7 @@ In this example we can see how to discover the node items: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); Node node = mgr.getNode("testNode"); // Discover the node items @@ -394,7 +394,7 @@ In this example we can see how to get node subscriptions: ``` // Create a pubsub manager using an existing XMPPConnection -PubSubManager mgr = new PubSubManager(con); +PubSubManager mgr = PubSubManager.getInstanceFor(con); Node node = mgr.getNode("testNode"); // Discover the node subscriptions diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/address/MultipleRecipientManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/address/MultipleRecipientManager.java index adc4fd810..f5b6c6de2 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/address/MultipleRecipientManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/address/MultipleRecipientManager.java @@ -283,11 +283,7 @@ public class MultipleRecipientManager { */ private static DomainBareJid getMultipleRecipienServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); - List services = sdm.findServices(MultipleAddresses.NAMESPACE, true, true); - if (services.size() > 0) { - return services.get(0); - } - return null; + return sdm.findService(MultipleAddresses.NAMESPACE, true); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java index 0193848e2..68d96b523 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java @@ -684,7 +684,7 @@ public final class ServiceDiscoveryManager extends Manager { * Create a cache to hold the 25 most recently lookup services for a given feature for a period * of 24 hours. */ - private Cache> services = new ExpirationCache<>(25, + private Cache> services = new ExpirationCache<>(25, 24 * 60 * 60 * 1000); /** @@ -699,17 +699,17 @@ public final class ServiceDiscoveryManager extends Manager { * @throws NotConnectedException * @throws InterruptedException */ - public List findServices(String feature, boolean stopOnFirst, boolean useCache) + public List findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - List serviceAddresses = null; + List serviceDiscoInfo = null; DomainBareJid serviceName = connection().getServiceName(); if (useCache) { - serviceAddresses = services.get(feature); - if (serviceAddresses != null) { - return serviceAddresses; + serviceDiscoInfo = services.get(feature); + if (serviceDiscoInfo != null) { + return serviceDiscoInfo; } } - serviceAddresses = new LinkedList<>(); + serviceDiscoInfo = new LinkedList<>(); // Send the disco packet to the server itself DiscoverInfo info; try { @@ -717,17 +717,17 @@ public final class ServiceDiscoveryManager extends Manager { } catch (XMPPErrorException e) { // Be extra robust here: Return the empty linked list and log this situation LOGGER.log(Level.WARNING, "Could not discover information about service", e); - return serviceAddresses; + return serviceDiscoInfo; } - // Check if the server supports XEP-33 + // Check if the server supports the feature if (info.containsFeature(feature)) { - serviceAddresses.add(serviceName); + serviceDiscoInfo.add(info); if (stopOnFirst) { if (useCache) { // Cache the discovered information - services.put(feature, serviceAddresses); + services.put(feature, serviceDiscoInfo); } - return serviceAddresses; + return serviceDiscoInfo; } } DiscoverItems items; @@ -736,7 +736,7 @@ public final class ServiceDiscoveryManager extends Manager { items = discoverItems(serviceName); } catch(XMPPErrorException e) { LOGGER.log(Level.WARNING, "Could not discover items about service", e); - return serviceAddresses; + return serviceDiscoInfo; } for (DiscoverItems.Item item : items.getItems()) { try { @@ -752,7 +752,8 @@ public final class ServiceDiscoveryManager extends Manager { continue; } if (info.containsFeature(feature)) { - serviceAddresses.add(item.getEntityID().asDomainBareJid()); + serviceDiscoInfo.add(info); + //serviceAddresses.add(item.getEntityID().asDomainBareJid()); if (stopOnFirst) { break; } @@ -760,9 +761,54 @@ public final class ServiceDiscoveryManager extends Manager { } if (useCache) { // Cache the discovered information - services.put(feature, serviceAddresses); + services.put(feature, serviceDiscoInfo); } - return serviceAddresses; + return serviceDiscoInfo; + } + + /** + * Find all services under the users service that provide a given feature. + * + * @param feature the feature to search for + * @param stopOnFirst if true, stop searching after the first service was found + * @param useCache if true, query a cache first to avoid network I/O + * @return a possible empty list of services providing the given feature + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public List findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + List services = findServicesDiscoverInfo(feature, stopOnFirst, useCache); + List res = new ArrayList<>(services.size()); + for (DiscoverInfo info : services) { + res.add(info.getFrom().asDomainBareJid()); + } + return res; + } + + public DomainBareJid findService(String feature, boolean useCache, String category, String type) + throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + List services = findServicesDiscoverInfo(feature, true, useCache); + if (services.isEmpty()) { + return null; + } + DiscoverInfo info = services.get(0); + if (category != null && type != null) { + if (!info.hasIdentity(category, type)) { + return null; + } + } + else if (category != null || type != null) { + throw new IllegalArgumentException("Must specify either both, category and type, or none"); + } + return info.getFrom().asDomainBareJid(); + } + + public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException { + return findService(feature, useCache, null, null); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/CollectionNode.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/CollectionNode.java index 623bbccdd..4b93f3bd5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/CollectionNode.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/CollectionNode.java @@ -16,13 +16,11 @@ */ package org.jivesoftware.smackx.pubsub; -import org.jivesoftware.smack.XMPPConnection; - public class CollectionNode extends Node { - CollectionNode(XMPPConnection connection, String nodeId) + CollectionNode(PubSubManager pubSubManager, String nodeId) { - super(connection, nodeId); + super(pubSubManager, nodeId); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java index 676a4cac5..3da8c9be0 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/LeafNode.java @@ -22,7 +22,6 @@ import java.util.List; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.ExtensionElement; @@ -39,9 +38,9 @@ import org.jivesoftware.smackx.pubsub.packet.PubSub; */ public class LeafNode extends Node { - LeafNode(XMPPConnection connection, String nodeName) + LeafNode(PubSubManager pubSubManager, String nodeId) { - super(connection, nodeName); + super(pubSubManager, nodeId); } /** @@ -57,9 +56,9 @@ public class LeafNode extends Node public DiscoverItems discoverItems() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { DiscoverItems items = new DiscoverItems(); - items.setTo(to); + items.setTo(pubSubManager.getServiceJid()); items.setNode(getId()); - return (DiscoverItems) con.createPacketCollectorAndSend(items).nextResultOrThrow(); + return pubSubManager.getConnection().createPacketCollectorAndSend(items).nextResultOrThrow(); } /** @@ -193,7 +192,7 @@ public class LeafNode extends Node private List getItems(PubSub request, List returnedExtensions) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - PubSub result = con.createPacketCollectorAndSend(request).nextResultOrThrow(); + PubSub result = pubSubManager.getConnection().createPacketCollectorAndSend(request).nextResultOrThrow(); ItemsExtension itemsElem = result.getExtension(PubSubElementType.ITEMS); if (returnedExtensions != null) { returnedExtensions.addAll(result.getExtensions()); @@ -219,7 +218,7 @@ public class LeafNode extends Node { PubSub packet = createPubsubPacket(Type.set, new NodeExtension(PubSubElementType.PUBLISH, getId())); - con.sendStanza(packet); + pubSubManager.getConnection().sendStanza(packet); } /** @@ -266,7 +265,7 @@ public class LeafNode extends Node { PubSub packet = createPubsubPacket(Type.set, new PublishItem(getId(), items)); - con.sendStanza(packet); + pubSubManager.getConnection().sendStanza(packet); } /** @@ -290,7 +289,7 @@ public class LeafNode extends Node { PubSub packet = createPubsubPacket(Type.set, new NodeExtension(PubSubElementType.PUBLISH, getId())); - con.createPacketCollectorAndSend(packet).nextResultOrThrow(); + pubSubManager.getConnection().createPacketCollectorAndSend(packet).nextResultOrThrow(); } /** @@ -347,7 +346,7 @@ public class LeafNode extends Node { PubSub packet = createPubsubPacket(Type.set, new PublishItem(getId(), items)); - con.createPacketCollectorAndSend(packet).nextResultOrThrow(); + pubSubManager.getConnection().createPacketCollectorAndSend(packet).nextResultOrThrow(); } /** @@ -364,7 +363,7 @@ public class LeafNode extends Node { PubSub request = createPubsubPacket(Type.set, new NodeExtension(PubSubElementType.PURGE_OWNER, getId()), PubSubElementType.PURGE_OWNER.getNamespace()); - con.createPacketCollectorAndSend(request).nextResultOrThrow(); + pubSubManager.getConnection().createPacketCollectorAndSend(request).nextResultOrThrow(); } /** @@ -401,6 +400,6 @@ public class LeafNode extends Node items.add(new Item(id)); } PubSub request = createPubsubPacket(Type.set, new ItemsExtension(ItemsExtension.ItemsElementType.retract, getId(), items)); - con.createPacketCollectorAndSend(request).nextResultOrThrow(); + pubSubManager.getConnection().createPacketCollectorAndSend(request).nextResultOrThrow(); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java index abbc6db7a..3bfcf35d9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Node.java @@ -24,7 +24,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.OrFilter; import org.jivesoftware.smack.filter.StanzaFilter; @@ -43,13 +42,11 @@ import org.jivesoftware.smackx.pubsub.util.NodeUtils; import org.jivesoftware.smackx.shim.packet.Header; import org.jivesoftware.smackx.shim.packet.HeadersExtension; import org.jivesoftware.smackx.xdata.Form; -import org.jxmpp.jid.Jid; abstract public class Node { - protected XMPPConnection con; - protected String id; - protected Jid to; + protected final PubSubManager pubSubManager; + protected final String id; protected ConcurrentHashMap, StanzaListener> itemEventToListenerMap = new ConcurrentHashMap, StanzaListener>(); protected ConcurrentHashMap itemDeleteToListenerMap = new ConcurrentHashMap(); @@ -62,21 +59,10 @@ abstract public class Node * @param connection The connection the node is associated with * @param nodeName The node id */ - Node(XMPPConnection connection, String nodeName) + Node(PubSubManager pubSubManager, String nodeId) { - con = connection; - id = nodeName; - } - - /** - * Some XMPP servers may require a specific service to be addressed on the - * server. - * - * For example, OpenFire requires the server to be prefixed by pubsub - */ - void setTo(Jid toAddress) - { - to = toAddress; + this.pubSubManager = pubSubManager; + id = nodeId; } /** @@ -119,7 +105,7 @@ abstract public class Node { PubSub packet = createPubsubPacket(Type.set, new FormNode(FormNodeType.CONFIGURE_OWNER, getId(), submitForm), PubSubNamespace.OWNER); - con.createPacketCollectorAndSend(packet).nextResultOrThrow(); + pubSubManager.getConnection().createPacketCollectorAndSend(packet).nextResultOrThrow(); } /** @@ -134,9 +120,9 @@ abstract public class Node public DiscoverInfo discoverInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { DiscoverInfo info = new DiscoverInfo(); - info.setTo(to); + info.setTo(pubSubManager.getServiceJid()); info.setNode(getId()); - return (DiscoverInfo) con.createPacketCollectorAndSend(info).nextResultOrThrow(); + return pubSubManager.getConnection().createPacketCollectorAndSend(info).nextResultOrThrow(); } /** @@ -336,7 +322,7 @@ abstract public class Node PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); // CHECKSTYLE:ON request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); - PubSub reply = PubSubManager.sendPubsubPacket(con, request); + PubSub reply = sendPubsubPacket(request); return reply.getExtension(PubSubElementType.SUBSCRIPTION); } @@ -420,7 +406,7 @@ abstract public class Node { StanzaListener conListener = new ItemEventTranslator(listener); itemEventToListenerMap.put(listener, conListener); - con.addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); + pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); } /** @@ -433,7 +419,7 @@ abstract public class Node StanzaListener conListener = itemEventToListenerMap.remove(listener); if (conListener != null) - con.removeSyncStanzaListener(conListener); + pubSubManager.getConnection().removeSyncStanzaListener(conListener); } /** @@ -446,7 +432,7 @@ abstract public class Node { StanzaListener conListener = new NodeConfigTranslator(listener); configEventToListenerMap.put(listener, conListener); - con.addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); + pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); } /** @@ -459,7 +445,7 @@ abstract public class Node StanzaListener conListener = configEventToListenerMap .remove(listener); if (conListener != null) - con.removeSyncStanzaListener(conListener); + pubSubManager.getConnection().removeSyncStanzaListener(conListener); } /** @@ -475,7 +461,7 @@ abstract public class Node EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract"); EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString()); - con.addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge)); + pubSubManager.getConnection().addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge)); } /** @@ -488,7 +474,7 @@ abstract public class Node StanzaListener conListener = itemDeleteToListenerMap .remove(listener); if (conListener != null) - con.removeSyncStanzaListener(conListener); + pubSubManager.getConnection().removeSyncStanzaListener(conListener); } @Override @@ -504,12 +490,12 @@ abstract public class Node protected PubSub createPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) { - return PubSub.createPubsubPacket(to, type, ext, ns); + return PubSub.createPubsubPacket(pubSubManager.getServiceJid(), type, ext, ns); } protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - return PubSubManager.sendPubsubPacket(con, packet); + return pubSubManager.sendPubsubPacket(packet); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java index 821b27a52..0a5f2d572 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java @@ -17,16 +17,22 @@ package org.jivesoftware.smackx.pubsub; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.EmptyResultIQ; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.ExtensionElement; @@ -52,24 +58,73 @@ import org.jxmpp.stringprep.XmppStringprepException; * * @author Robin Collier */ -final public class PubSubManager -{ - private XMPPConnection con; - private DomainBareJid to; - private Map nodeMap = new ConcurrentHashMap(); +public final class PubSubManager extends Manager { - /** - * Create a pubsub manager associated to the specified connection. Defaults the service - * name to pubsub - * - * @param connection The XMPP connection - * @throws XmppStringprepException - */ - public PubSubManager(XMPPConnection connection) throws XmppStringprepException - { - con = connection; - to = JidCreate.domainBareFrom("pubsub." + connection.getServiceName()); - } + private static final Logger LOGGER = Logger.getLogger(PubSubManager.class.getName()); + private static final Map> INSTANCES = new WeakHashMap<>(); + + /** + * The JID of the PubSub service this manager manages. + */ + private final DomainBareJid pubSubService; + + /** + * A map of node IDs to Nodes, used to cache those Nodes. This does only cache the type of Node, + * i.e. {@link CollectionNode} or {@link LeafNode}. + */ + private final Map nodeMap = new ConcurrentHashMap(); + + /** + * Get a PubSub manager for the default PubSub service of the connection. + * + * @param connection + * @return the default PubSub manager. + */ + public static PubSubManager getInstance(XMPPConnection connection) { + DomainBareJid pubSubService = null; + if (connection.isAuthenticated()) { + try { + pubSubService = getPubSubService(connection); + } + catch (NoResponseException | XMPPErrorException | NotConnectedException e) { + LOGGER.log(Level.WARNING, "Could not determine PubSub service", e); + } + catch (InterruptedException e) { + LOGGER.log(Level.FINE, "Interupted while trying to determine PubSub service", e); + } + } + if (pubSubService == null) { + try { + // Perform an educated guess about what the PubSub service's domain bare JID may be + pubSubService = JidCreate.domainBareFrom("pubsub." + connection.getServiceName()); + } + catch (XmppStringprepException e) { + throw new RuntimeException(e); + } + } + return getInstance(connection, pubSubService); + } + + /** + * Get the PubSub manager for the given connection and PubSub service. + * + * @param connection the XMPP connection. + * @param pubSubService the PubSub service. + * @return a PubSub manager for the connection and service. + */ + public static synchronized PubSubManager getInstance(XMPPConnection connection, DomainBareJid pubSubService) { + Map managers = INSTANCES.get(connection); + if (managers == null) { + managers = new HashMap<>(); + INSTANCES.put(connection, managers); + } + PubSubManager pubSubManager = managers.get(pubSubService); + if (pubSubManager == null) { + pubSubManager = new PubSubManager(connection, pubSubService); + managers.put(pubSubService, pubSubManager); + } + return pubSubManager; + } /** * Create a pubsub manager associated to the specified connection where @@ -78,10 +133,10 @@ final public class PubSubManager * @param connection The XMPP connection * @param toAddress The pubsub specific to address (required for some servers) */ - public PubSubManager(XMPPConnection connection, DomainBareJid toAddress) + PubSubManager(XMPPConnection connection, DomainBareJid toAddress) { - con = connection; - to = toAddress; + super(connection); + pubSubService = toAddress; } /** @@ -98,8 +153,7 @@ final public class PubSubManager PubSub reply = sendPubsubPacket(Type.set, new NodeExtension(PubSubElementType.CREATE), null); NodeExtension elem = reply.getExtension("create", PubSubNamespace.BASIC.getXmlns()); - LeafNode newNode = new LeafNode(con, elem.getNode()); - newNode.setTo(to); + LeafNode newNode = new LeafNode(this, elem.getNode()); nodeMap.put(newNode.getId(), newNode); return newNode; @@ -108,7 +162,7 @@ final public class PubSubManager /** * Creates a node with default configuration. * - * @param id The id of the node, which must be unique within the + * @param nodeId The id of the node, which must be unique within the * pubsub service * @return The node that was created * @throws XMPPErrorException @@ -116,9 +170,9 @@ final public class PubSubManager * @throws NotConnectedException * @throws InterruptedException */ - public LeafNode createNode(String id) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException + public LeafNode createNode(String nodeId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - return (LeafNode)createNode(id, null); + return (LeafNode) createNode(nodeId, null); } /** @@ -126,7 +180,7 @@ final public class PubSubManager * * Note: This is the only way to create a collection node. * - * @param name The name of the node, which must be unique within the + * @param nodeId The name of the node, which must be unique within the * pubsub service * @param config The configuration for the node * @return The node that was created @@ -135,9 +189,9 @@ final public class PubSubManager * @throws NotConnectedException * @throws InterruptedException */ - public Node createNode(String name, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException + public Node createNode(String nodeId, Form config) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - PubSub request = PubSub.createPubsubPacket(to, Type.set, new NodeExtension(PubSubElementType.CREATE, name), null); + PubSub request = PubSub.createPubsubPacket(pubSubService, Type.set, new NodeExtension(PubSubElementType.CREATE, nodeId), null); boolean isLeafNode = true; if (config != null) @@ -151,9 +205,8 @@ final public class PubSubManager // Errors will cause exceptions in getReply, so it only returns // on success. - sendPubsubPacket(con, request); - Node newNode = isLeafNode ? new LeafNode(con, name) : new CollectionNode(con, name); - newNode.setTo(to); + sendPubsubPacket(request); + Node newNode = isLeafNode ? new LeafNode(this, nodeId) : new CollectionNode(this, nodeId); nodeMap.put(newNode.getId(), newNode); return newNode; @@ -177,16 +230,16 @@ final public class PubSubManager if (node == null) { DiscoverInfo info = new DiscoverInfo(); - info.setTo(to); + info.setTo(pubSubService); info.setNode(id); - DiscoverInfo infoReply = (DiscoverInfo) con.createPacketCollectorAndSend(info).nextResultOrThrow(); + DiscoverInfo infoReply = connection().createPacketCollectorAndSend(info).nextResultOrThrow(); if (infoReply.hasIdentity(PubSub.ELEMENT, "leaf")) { - node = new LeafNode(con, id); + node = new LeafNode(this, id); } else if (infoReply.hasIdentity(PubSub.ELEMENT, "collection")) { - node = new CollectionNode(con, id); + node = new CollectionNode(this, id); } else { // XEP-60 5.3 states that @@ -194,12 +247,11 @@ final public class PubSubManager // If this is not the case, then we are dealing with an PubSub implementation that doesn't follow the specification. throw new AssertionError( "PubSub service '" - + to + + pubSubService + "' returned disco info result for node '" + id + "', 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."); } - node.setTo(to); nodeMap.put(id, node); } @SuppressWarnings("unchecked") @@ -229,8 +281,8 @@ final public class PubSubManager if (nodeId != null) items.setNode(nodeId); - items.setTo(to); - DiscoverItems nodeItems = (DiscoverItems) con.createPacketCollectorAndSend(items).nextResultOrThrow(); + items.setTo(pubSubService); + DiscoverItems nodeItems = connection().createPacketCollectorAndSend(items).nextResultOrThrow(); return nodeItems; } @@ -299,6 +351,15 @@ final public class PubSubManager return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT); } + /** + * Get the JID of the PubSub service managed by this manager. + * + * @return the JID of the PubSub service. + */ + public DomainBareJid getServiceJid() { + return pubSubService; + } + /** * Gets the supported features of the servers pubsub implementation * as a standard {@link DiscoverInfo} instance. @@ -311,33 +372,92 @@ final public class PubSubManager */ public DiscoverInfo getSupportedFeatures() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(con); - return mgr.discoverInfo(to); + ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(connection()); + return mgr.discoverInfo(pubSubService); } + /** + * Check if it is possible to create PubSub nodes on this service. It could be possible that the + * PubSub service allows only certain XMPP entities (clients) to create nodes and publish items + * to them. + *

+ * Note that since XEP-60 does not provide an API to determine if an XMPP entity is allowed to + * create nodes, therefore this method creates an instant node calling {@link #createNode()} to + * determine if it is possible to create nodes. + *

+ * + * @return true if it is possible to create nodes, false otherwise. + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws XMPPErrorException + */ + public boolean canCreateNodesAndPublishItems() throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException { + LeafNode leafNode = null; + try { + leafNode = createNode(); + } + catch (XMPPErrorException e) { + if (e.getXMPPError().getCondition() == XMPPError.Condition.forbidden) { + return false; + } + throw e; + } finally { + if (leafNode != null) { + deleteNode(leafNode.getId()); + } + } + return true; + } + private PubSub sendPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - return sendPubsubPacket(con, to, type, Collections.singletonList(ext), ns); + return sendPubsubPacket(pubSubService, type, Collections.singletonList(ext), ns); } - static PubSub sendPubsubPacket(XMPPConnection con, Jid to, Type type, List extList, PubSubNamespace ns) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException - { + XMPPConnection getConnection() { + return connection(); + } + + PubSub sendPubsubPacket(Jid to, Type type, List extList, PubSubNamespace ns) + throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { // CHECKSTYLE:OFF PubSub pubSub = new PubSub(to, type, ns); for (ExtensionElement pe : extList) { pubSub.addExtension(pe); } // CHECKSTYLE:ON - return sendPubsubPacket(con ,pubSub); + return sendPubsubPacket(pubSub); } - static PubSub sendPubsubPacket(XMPPConnection con, PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException - { - IQ resultIQ = con.createPacketCollectorAndSend(packet).nextResultOrThrow(); + PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException { + IQ resultIQ = connection().createPacketCollectorAndSend(packet).nextResultOrThrow(); if (resultIQ instanceof EmptyResultIQ) { return null; } return (PubSub) resultIQ; } + /** + * Get the "default" PubSub service for a given XMPP connection. The default PubSub service is + * simply an arbitrary XMPP service with the PubSub feature and an identity of category "pubsub" + * and type "service". + * + * @param connection + * @return the default PubSub service or null. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @see XEP-60 ยง 5.1 Discover + * Features + */ + public static DomainBareJid getPubSubService(XMPPConnection connection) + throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + return ServiceDiscoveryManager.getInstanceFor(connection).findService(PubSub.NAMESPACE, + true, "pubsub", "service"); + } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/listener/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/listener/package-info.java index 58086241d..243e0b90a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/listener/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/listener/package-info.java @@ -16,6 +16,6 @@ */ /** - * TODO describe me. + * Listeners for Publish-Subscribe (XEP-60) events. */ package org.jivesoftware.smackx.pubsub.listener; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java index 14f482b08..ae9bd4777 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java @@ -16,6 +16,6 @@ */ /** - * TODO describe me. + * Smack's API for XEP-60: Publish-Subscribe. */ package org.jivesoftware.smackx.pubsub; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/package-info.java index 003aefb60..8b2dad431 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/packet/package-info.java @@ -16,6 +16,6 @@ */ /** - * TODO describe me. + * Stanzas and extension elements for Publish-Subscribe (XEP-60). */ package org.jivesoftware.smackx.pubsub.packet; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/package-info.java index 88d58f215..4ae3226cd 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/package-info.java @@ -16,6 +16,6 @@ */ /** - * TODO describe me. + * Providers for Publish-Subscribe (XEP-60). */ package org.jivesoftware.smackx.pubsub.provider; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/package-info.java index 21e7cb360..bd9efb304 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/package-info.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/util/package-info.java @@ -16,6 +16,6 @@ */ /** - * TODO describe me. + * Utilities for Publish-Subscribe (XEP-60). */ package org.jivesoftware.smackx.pubsub.util; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java index 698a15e12..6c116f643 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java @@ -54,7 +54,7 @@ public class ConfigureFormTest public void getConfigFormWithInsufficientPriviliges() throws XMPPException, SmackException, IOException, InterruptedException { ThreadedDummyConnection con = ThreadedDummyConnection.newInstance(); - PubSubManager mgr = new PubSubManager(con); + PubSubManager mgr = new PubSubManager(con, PubSubManagerTest.DUMMY_PUBSUB_SERVICE); DiscoverInfo info = new DiscoverInfo(); Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); @@ -81,7 +81,7 @@ public class ConfigureFormTest public void getConfigFormWithTimeout() throws XMPPException, SmackException, InterruptedException, XmppStringprepException { ThreadedDummyConnection con = new ThreadedDummyConnection(); - PubSubManager mgr = new PubSubManager(con); + PubSubManager mgr = new PubSubManager(con, PubSubManagerTest.DUMMY_PUBSUB_SERVICE); DiscoverInfo info = new DiscoverInfo(); Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/PubSubManagerTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/PubSubManagerTest.java index f90cf1736..922aaed11 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/PubSubManagerTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/PubSubManagerTest.java @@ -25,13 +25,29 @@ import org.jivesoftware.smack.ThreadedDummyConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.junit.Test; +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; public class PubSubManagerTest { + public static final DomainBareJid DUMMY_PUBSUB_SERVICE; + + static { + DomainBareJid pubSubService; + try { + pubSubService = JidCreate.domainBareFrom("pubsub.dummy.org"); + } + catch (XmppStringprepException e) { + throw new AssertionError(e); + } + DUMMY_PUBSUB_SERVICE = pubSubService; + } + @Test public void deleteNodeTest() throws InterruptedException, SmackException, IOException, XMPPException { ThreadedDummyConnection con = ThreadedDummyConnection.newInstance(); - PubSubManager mgr = new PubSubManager(con); + PubSubManager mgr = new PubSubManager(con, DUMMY_PUBSUB_SERVICE); mgr.deleteNode("foo@bar.org"); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java new file mode 100644 index 000000000..ed4943a50 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java @@ -0,0 +1,66 @@ +/** + * + * Copyright 2015 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.pubsub; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jxmpp.jid.DomainBareJid; + +public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { + + private final PubSubManager pubSubManagerOne; + + public PubSubIntegrationTest(SmackIntegrationTestEnvironment environment) + throws TestNotPossibleException, NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException { + super(environment); + DomainBareJid pubSubService = PubSubManager.getPubSubService(conOne); + if (pubSubService == null) { + throw new TestNotPossibleException("No PubSub service found"); + } + pubSubManagerOne = PubSubManager.getInstance(conOne, pubSubService); + if (!pubSubManagerOne.canCreateNodesAndPublishItems()) { + throw new TestNotPossibleException("PubSub service does not allow node creation"); + } + } + + @SmackIntegrationTest + public void simplePubSubNodeTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + final String nodename = "sinttest-simple-nodename-" + testRunId; + final String itemId = "sintest-simple-itemid-" + testRunId; + LeafNode leafNode = pubSubManagerOne.createNode(nodename); + try { + leafNode.publish(new Item(itemId)); + List items = leafNode.getItems(); + assertEquals(1, items.size()); + Item item = items.get(0); + assertEquals(itemId, item.getId()); + } + finally { + pubSubManagerOne.deleteNode(nodename); + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java new file mode 120000 index 000000000..2ac8fc1a3 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/package-info.java \ No newline at end of file