/*
* Created on 2009-07-09
*/
package org.jivesoftware.smackx.pubsub;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.OrFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.packet.DelayInformation;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.Header;
import org.jivesoftware.smackx.packet.HeadersExtension;
import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
import org.jivesoftware.smackx.pubsub.util.NodeUtils;
abstract public class Node {
/**
* Filter for {@link PacketListener} to filter out events not specific to
* the event type expected for this node.
*
* @author Robin Collier
*/
class EventContentFilter implements PacketFilter {
private final String firstElement;
private String secondElement;
EventContentFilter(String elementName) {
firstElement = elementName;
}
EventContentFilter(String firstLevelEelement, String secondLevelElement) {
firstElement = firstLevelEelement;
secondElement = secondLevelElement;
}
@Override
public boolean accept(Packet packet) {
if (!(packet instanceof Message)) {
return false;
}
final EventElement event = (EventElement) packet.getExtension(
"event", PubSubNamespace.EVENT.getXmlns());
if (event == null) {
return false;
}
final NodeExtension embedEvent = event.getEvent();
if (embedEvent == null) {
return false;
}
if (embedEvent.getElementName().equals(firstElement)) {
if (!embedEvent.getNode().equals(getId())) {
return false;
}
if (secondElement == null) {
return true;
}
if (embedEvent instanceof EmbeddedPacketExtension) {
final List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension) embedEvent)
.getExtensions();
if (secondLevelList.size() > 0
&& secondLevelList.get(0).getElementName()
.equals(secondElement)) {
return true;
}
}
}
return false;
}
}
/**
* This class translates low level item deletion events into api level
* objects for user consumption.
*
* @author Robin Collier
*/
public class ItemDeleteTranslator implements PacketListener {
private final ItemDeleteListener listener;
public ItemDeleteTranslator(ItemDeleteListener eventListener) {
listener = eventListener;
}
@Override
public void processPacket(Packet packet) {
final EventElement event = (EventElement) packet.getExtension(
"event", PubSubNamespace.EVENT.getXmlns());
final List<PacketExtension> extList = event.getExtensions();
if (extList.get(0).getElementName()
.equals(PubSubElementType.PURGE_EVENT.getElementName())) {
listener.handlePurge();
} else {
final ItemsExtension itemsElem = (ItemsExtension) event
.getEvent();
final Collection<? extends PacketExtension> pubItems = itemsElem
.getItems();
final Iterator<RetractItem> it = (Iterator<RetractItem>) pubItems
.iterator();
final List<String> items = new ArrayList<String>(
pubItems.size());
while (it.hasNext()) {
final RetractItem item = it.next();
items.add(item.getId());
}
final ItemDeleteEvent eventItems = new ItemDeleteEvent(
itemsElem.getNode(), items, getSubscriptionIds(packet));
listener.handleDeletedItems(eventItems);
}
}
}
/**
* This class translates low level item publication events into api level
* objects for user consumption.
*
* @author Robin Collier
*/
public class ItemEventTranslator implements PacketListener {
private final ItemEventListener listener;
public ItemEventTranslator(ItemEventListener eventListener) {
listener = eventListener;
}
@Override
public void processPacket(Packet packet) {
final EventElement event = (EventElement) packet.getExtension(
"event", PubSubNamespace.EVENT.getXmlns());
final ItemsExtension itemsElem = (ItemsExtension) event.getEvent();
DelayInformation delay = (DelayInformation) packet.getExtension(
"delay", "urn:xmpp:delay");
// If there was no delay based on XEP-0203, then try XEP-0091 for
// backward compatibility
if (delay == null) {
delay = (DelayInformation) packet.getExtension("x",
"jabber:x:delay");
}
final ItemPublishEvent eventItems = new ItemPublishEvent(
itemsElem.getNode(), itemsElem.getItems(),
getSubscriptionIds(packet), (delay == null ? null
: delay.getStamp()));
listener.handlePublishedItems(eventItems);
}
}
/**
* This class translates low level node configuration events into api level
* objects for user consumption.
*
* @author Robin Collier
*/
public class NodeConfigTranslator implements PacketListener {
private final NodeConfigListener listener;
public NodeConfigTranslator(NodeConfigListener eventListener) {
listener = eventListener;
}
@Override
public void processPacket(Packet packet) {
final EventElement event = (EventElement) packet.getExtension(
"event", PubSubNamespace.EVENT.getXmlns());
final ConfigurationEvent config = (ConfigurationEvent) event
.getEvent();
listener.handleNodeConfiguration(config);
}
}
private static List<String> getSubscriptionIds(Packet packet) {
final HeadersExtension headers = (HeadersExtension) packet
.getExtension("headers", "http://jabber.org/protocol/shim");
List<String> values = null;
if (headers != null) {
values = new ArrayList<String>(headers.getHeaders().size());
for (final Header header : headers.getHeaders()) {
values.add(header.getValue());
}
}
return values;
}
protected Connection con;
protected String id;
protected String to;
protected ConcurrentHashMap<ItemEventListener, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener, PacketListener>();
protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();
protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();
/**
* Construct a node associated to the supplied connection with the specified
* node id.
*
* @param connection
* The connection the node is associated with
* @param nodeName
* The node id
*/
Node(Connection connection, String nodeName) {
con = connection;
id = nodeName;
}
/**
* Register a listener for configuration events. This listener will get
* called whenever the node's configuration changes.
*
* @param listener
* The handler for the event
*/
public void addConfigurationListener(NodeConfigListener listener) {
final PacketListener conListener = new NodeConfigTranslator(listener);
configEventToListenerMap.put(listener, conListener);
con.addPacketListener(conListener, new EventContentFilter(
EventElementType.configuration.toString()));
}
/**
* Register an listener for item delete events. This listener gets called
* whenever an item is deleted from the node.
*
* @param listener
* The handler for the event
*/
public void addItemDeleteListener(ItemDeleteListener listener) {
final PacketListener delListener = new ItemDeleteTranslator(listener);
itemDeleteToListenerMap.put(listener, delListener);
final EventContentFilter deleteItem = new EventContentFilter(
EventElementType.items.toString(), "retract");
final EventContentFilter purge = new EventContentFilter(
EventElementType.purge.toString());
con.addPacketListener(delListener, new OrFilter(deleteItem, purge));
}
/**
* Register a listener for item publication events. This listener will get
* called whenever an item is published to this node.
*
* @param listener
* The handler for the event
*/
public void addItemEventListener(ItemEventListener listener) {
final PacketListener conListener = new ItemEventTranslator(listener);
itemEventToListenerMap.put(listener, conListener);
con.addPacketListener(conListener, new EventContentFilter(
EventElementType.items.toString(), "item"));
}
protected PubSub createPubsubPacket(Type type, PacketExtension ext) {
return createPubsubPacket(type, ext, null);
}
protected PubSub createPubsubPacket(Type type, PacketExtension ext,
PubSubNamespace ns) {
return PubSubManager.createPubsubPacket(to, type, ext, ns);
}
/**
* Discover node information in standard {@link DiscoverInfo} format.
*
* @return The discovery information about the node.
*
* @throws XMPPException
*/
public DiscoverInfo discoverInfo() throws XMPPException {
final DiscoverInfo info = new DiscoverInfo();
info.setTo(to);
info.setNode(getId());
return (DiscoverInfo) SyncPacketSend.getReply(con, info);
}
/**
* Get the NodeId
*
* @return the node id
*/
public String getId() {
return id;
}
/**
* Returns a configuration form, from which you can create an answer form to
* be submitted via the {@link #sendConfigurationForm(Form)}.
*
* @return the configuration form
*/
public ConfigureForm getNodeConfiguration() throws XMPPException {
final Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(
PubSubElementType.CONFIGURE_OWNER, getId()),
PubSubNamespace.OWNER);
return NodeUtils.getFormFromPacket(reply,
PubSubElementType.CONFIGURE_OWNER);
}
/**
* Returns a SubscribeForm for subscriptions, from which you can create an
* answer form to be submitted via the {@link #sendConfigurationForm(Form)}.
*
* @return A subscription options form
*
* @throws XMPPException
*/
public SubscribeForm getSubscriptionOptions(String jid)
throws XMPPException {
return getSubscriptionOptions(jid, null);
}
/**
* Get the options for configuring the specified subscription.
*
* @param jid
* JID the subscription is registered under
* @param subscriptionId
* The subscription id
*
* @return The subscription option form
*
* @throws XMPPException
*/
public SubscribeForm getSubscriptionOptions(String jid,
String subscriptionId) throws XMPPException {
final PubSub packet = (PubSub) sendPubsubPacket(Type.GET,
new OptionsExtension(jid, getId(), subscriptionId));
final FormNode ext = (FormNode) packet
.getExtension(PubSubElementType.OPTIONS);
return new SubscribeForm(ext.getForm());
}
/**
* Get the subscriptions currently associated with this node.
*
* @return List of {@link Subscription}
*
* @throws XMPPException
*/
public List<Subscription> getSubscriptions() throws XMPPException {
final PubSub reply = (PubSub) sendPubsubPacket(Type.GET,
new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()));
final SubscriptionsExtension subElem = (SubscriptionsExtension) reply
.getExtension(PubSubElementType.SUBSCRIPTIONS);
return subElem.getSubscriptions();
}
/**
* Unregister a listener for configuration events.
*
* @param listener
* The handler to unregister
*/
public void removeConfigurationListener(NodeConfigListener listener) {
final PacketListener conListener = configEventToListenerMap
.remove(listener);
if (conListener != null) {
con.removePacketListener(conListener);
}
}
/**
* Unregister a listener for item delete events.
*
* @param listener
* The handler to unregister
*/
public void removeItemDeleteListener(ItemDeleteListener listener) {
final PacketListener conListener = itemDeleteToListenerMap
.remove(listener);
if (conListener != null) {
con.removePacketListener(conListener);
}
}
/**
* Unregister a listener for publication events.
*
* @param listener
* The handler to unregister
*/
public void removeItemEventListener(ItemEventListener listener) {
final PacketListener conListener = itemEventToListenerMap
.remove(listener);
if (conListener != null) {
con.removePacketListener(conListener);
}
}
/**
* Update the configuration with the contents of the new {@link Form}
*
* @param submitForm
*/
public void sendConfigurationForm(Form submitForm) throws XMPPException {
final PubSub packet = createPubsubPacket(Type.SET, new FormNode(
FormNodeType.CONFIGURE_OWNER, getId(), submitForm),
PubSubNamespace.OWNER);
SyncPacketSend.getReply(con, packet);
}
protected Packet sendPubsubPacket(Type type, NodeExtension ext)
throws XMPPException {
return PubSubManager.sendPubsubPacket(con, to, type, ext);
}
protected Packet sendPubsubPacket(Type type, NodeExtension ext,
PubSubNamespace ns) throws XMPPException {
return PubSubManager.sendPubsubPacket(con, to, type, ext, ns);
}
/**
* Some XMPP servers may require a specific service to be addressed on the
* server.
*
* For example, OpenFire requires the server to be prefixed by <b>pubsub</b>
*/
void setTo(String toAddress) {
to = toAddress;
}
/**
* The user subscribes to the node using the supplied jid. The bare jid
* portion of this one must match the jid for the connection.
*
* Please note that the {@link Subscription.State} should be checked on
* return since more actions may be required by the caller.
* {@link Subscription.State#pending} - The owner must approve the
* subscription request before messages will be received.
* {@link Subscription.State#unconfigured} - If the
* {@link Subscription#isConfigRequired()} is true, the caller must
* configure the subscription before messages will be received. If it is
* false the caller can configure it but is not required to do so.
*
* @param jid
* The jid to subscribe as.
* @return The subscription
* @exception XMPPException
*/
public Subscription subscribe(String jid) throws XMPPException {
final PubSub reply = (PubSub) sendPubsubPacket(Type.SET,
new SubscribeExtension(jid, getId()));
return (Subscription) reply
.getExtension(PubSubElementType.SUBSCRIPTION);
}
/**
* The user subscribes to the node using the supplied jid and subscription
* options. The bare jid portion of this one must match the jid for the
* connection.
*
* Please note that the {@link Subscription.State} should be checked on
* return since more actions may be required by the caller.
* {@link Subscription.State#pending} - The owner must approve the
* subscription request before messages will be received.
* {@link Subscription.State#unconfigured} - If the
* {@link Subscription#isConfigRequired()} is true, the caller must
* configure the subscription before messages will be received. If it is
* false the caller can configure it but is not required to do so.
*
* @param jid
* The jid to subscribe as.
* @return The subscription
* @exception XMPPException
*/
public Subscription subscribe(String jid, SubscribeForm subForm)
throws XMPPException {
final PubSub request = createPubsubPacket(Type.SET,
new SubscribeExtension(jid, getId()));
request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
final PubSub reply = (PubSub) PubSubManager.sendPubsubPacket(con, jid,
Type.SET, request);
return (Subscription) reply
.getExtension(PubSubElementType.SUBSCRIPTION);
}
@Override
public String toString() {
return super.toString() + " " + getClass().getName() + " id: " + id;
}
/**
* Remove the subscription related to the specified JID. This will only work
* if there is only 1 subscription. If there are multiple subscriptions, use
* {@link #unsubscribe(String, String)}.
*
* @param jid
* The JID used to subscribe to the node
*
* @throws XMPPException
*/
public void unsubscribe(String jid) throws XMPPException {
unsubscribe(jid, null);
}
/**
* Remove the specific subscription related to the specified JID.
*
* @param jid
* The JID used to subscribe to the node
* @param subscriptionId
* The id of the subscription being removed
*
* @throws XMPPException
*/
public void unsubscribe(String jid, String subscriptionId)
throws XMPPException {
sendPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(),
subscriptionId));
}
}