Home | History | Annotate | Download | only in pubsub
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2009 Robin Collier.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 package org.jivesoftware.smackx.pubsub;
     21 
     22 import java.util.ArrayList;
     23 import java.util.Collection;
     24 import java.util.Iterator;
     25 import java.util.List;
     26 import java.util.concurrent.ConcurrentHashMap;
     27 
     28 import org.jivesoftware.smack.PacketListener;
     29 import org.jivesoftware.smack.Connection;
     30 import org.jivesoftware.smack.XMPPException;
     31 import org.jivesoftware.smack.filter.OrFilter;
     32 import org.jivesoftware.smack.filter.PacketFilter;
     33 import org.jivesoftware.smack.packet.Message;
     34 import org.jivesoftware.smack.packet.Packet;
     35 import org.jivesoftware.smack.packet.PacketExtension;
     36 import org.jivesoftware.smack.packet.IQ.Type;
     37 import org.jivesoftware.smackx.Form;
     38 import org.jivesoftware.smackx.packet.DelayInformation;
     39 import org.jivesoftware.smackx.packet.DiscoverInfo;
     40 import org.jivesoftware.smackx.packet.Header;
     41 import org.jivesoftware.smackx.packet.HeadersExtension;
     42 import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
     43 import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
     44 import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
     45 import org.jivesoftware.smackx.pubsub.packet.PubSub;
     46 import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
     47 import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
     48 import org.jivesoftware.smackx.pubsub.util.NodeUtils;
     49 
     50 abstract public class Node
     51 {
     52 	protected Connection con;
     53 	protected String id;
     54 	protected String to;
     55 
     56 	protected ConcurrentHashMap<ItemEventListener<Item>, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, PacketListener>();
     57 	protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();
     58 	protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();
     59 
     60 	/**
     61 	 * Construct a node associated to the supplied connection with the specified
     62 	 * node id.
     63 	 *
     64 	 * @param connection The connection the node is associated with
     65 	 * @param nodeName The node id
     66 	 */
     67 	Node(Connection connection, String nodeName)
     68 	{
     69 		con = connection;
     70 		id = nodeName;
     71 	}
     72 
     73 	/**
     74 	 * Some XMPP servers may require a specific service to be addressed on the
     75 	 * server.
     76 	 *
     77 	 *   For example, OpenFire requires the server to be prefixed by <b>pubsub</b>
     78 	 */
     79 	void setTo(String toAddress)
     80 	{
     81 		to = toAddress;
     82 	}
     83 
     84 	/**
     85 	 * Get the NodeId
     86 	 *
     87 	 * @return the node id
     88 	 */
     89 	public String getId()
     90 	{
     91 		return id;
     92 	}
     93 	/**
     94 	 * Returns a configuration form, from which you can create an answer form to be submitted
     95 	 * via the {@link #sendConfigurationForm(Form)}.
     96 	 *
     97 	 * @return the configuration form
     98 	 */
     99 	public ConfigureForm getNodeConfiguration()
    100 		throws XMPPException
    101 	{
    102 		Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER);
    103 		return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER);
    104 	}
    105 
    106 	/**
    107 	 * Update the configuration with the contents of the new {@link Form}
    108 	 *
    109 	 * @param submitForm
    110 	 */
    111 	public void sendConfigurationForm(Form submitForm)
    112 		throws XMPPException
    113 	{
    114 		PubSub packet = createPubsubPacket(Type.SET, new FormNode(FormNodeType.CONFIGURE_OWNER, getId(), submitForm), PubSubNamespace.OWNER);
    115 		SyncPacketSend.getReply(con, packet);
    116 	}
    117 
    118 	/**
    119 	 * Discover node information in standard {@link DiscoverInfo} format.
    120 	 *
    121 	 * @return The discovery information about the node.
    122 	 *
    123 	 * @throws XMPPException
    124 	 */
    125 	public DiscoverInfo discoverInfo()
    126 		throws XMPPException
    127 	{
    128 		DiscoverInfo info = new DiscoverInfo();
    129 		info.setTo(to);
    130 		info.setNode(getId());
    131 		return (DiscoverInfo)SyncPacketSend.getReply(con, info);
    132 	}
    133 
    134 	/**
    135 	 * Get the subscriptions currently associated with this node.
    136 	 *
    137 	 * @return List of {@link Subscription}
    138 	 *
    139 	 * @throws XMPPException
    140 	 */
    141 	public List<Subscription> getSubscriptions()
    142 		throws XMPPException
    143 	{
    144 		PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()));
    145 		SubscriptionsExtension subElem = (SubscriptionsExtension)reply.getExtension(PubSubElementType.SUBSCRIPTIONS);
    146 		return subElem.getSubscriptions();
    147 	}
    148 
    149 	/**
    150 	 * The user subscribes to the node using the supplied jid.  The
    151 	 * bare jid portion of this one must match the jid for the connection.
    152 	 *
    153 	 * Please note that the {@link Subscription.State} should be checked
    154 	 * on return since more actions may be required by the caller.
    155 	 * {@link Subscription.State#pending} - The owner must approve the subscription
    156 	 * request before messages will be received.
    157 	 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true,
    158 	 * the caller must configure the subscription before messages will be received.  If it is false
    159 	 * the caller can configure it but is not required to do so.
    160 	 * @param jid The jid to subscribe as.
    161 	 * @return The subscription
    162 	 * @exception XMPPException
    163 	 */
    164 	public Subscription subscribe(String jid)
    165 		throws XMPPException
    166 	{
    167 		PubSub reply = (PubSub)sendPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
    168 		return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
    169 	}
    170 
    171 	/**
    172 	 * The user subscribes to the node using the supplied jid and subscription
    173 	 * options.  The bare jid portion of this one must match the jid for the
    174 	 * connection.
    175 	 *
    176 	 * Please note that the {@link Subscription.State} should be checked
    177 	 * on return since more actions may be required by the caller.
    178 	 * {@link Subscription.State#pending} - The owner must approve the subscription
    179 	 * request before messages will be received.
    180 	 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true,
    181 	 * the caller must configure the subscription before messages will be received.  If it is false
    182 	 * the caller can configure it but is not required to do so.
    183 	 * @param jid The jid to subscribe as.
    184 	 * @return The subscription
    185 	 * @exception XMPPException
    186 	 */
    187 	public Subscription subscribe(String jid, SubscribeForm subForm)
    188 		throws XMPPException
    189 	{
    190 		PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
    191 		request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
    192 		PubSub reply = (PubSub)PubSubManager.sendPubsubPacket(con, jid, Type.SET, request);
    193 		return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
    194 	}
    195 
    196 	/**
    197 	 * Remove the subscription related to the specified JID.  This will only
    198 	 * work if there is only 1 subscription.  If there are multiple subscriptions,
    199 	 * use {@link #unsubscribe(String, String)}.
    200 	 *
    201 	 * @param jid The JID used to subscribe to the node
    202 	 *
    203 	 * @throws XMPPException
    204 	 */
    205 	public void unsubscribe(String jid)
    206 		throws XMPPException
    207 	{
    208 		unsubscribe(jid, null);
    209 	}
    210 
    211 	/**
    212 	 * Remove the specific subscription related to the specified JID.
    213 	 *
    214 	 * @param jid The JID used to subscribe to the node
    215 	 * @param subscriptionId The id of the subscription being removed
    216 	 *
    217 	 * @throws XMPPException
    218 	 */
    219 	public void unsubscribe(String jid, String subscriptionId)
    220 		throws XMPPException
    221 	{
    222 		sendPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(), subscriptionId));
    223 	}
    224 
    225 	/**
    226 	 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
    227 	 * via the {@link #sendConfigurationForm(Form)}.
    228 	 *
    229 	 * @return A subscription options form
    230 	 *
    231 	 * @throws XMPPException
    232 	 */
    233 	public SubscribeForm getSubscriptionOptions(String jid)
    234 		throws XMPPException
    235 	{
    236 		return getSubscriptionOptions(jid, null);
    237 	}
    238 
    239 
    240 	/**
    241 	 * Get the options for configuring the specified subscription.
    242 	 *
    243 	 * @param jid JID the subscription is registered under
    244 	 * @param subscriptionId The subscription id
    245 	 *
    246 	 * @return The subscription option form
    247 	 *
    248 	 * @throws XMPPException
    249 	 */
    250 	public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId)
    251 		throws XMPPException
    252 	{
    253 		PubSub packet = (PubSub)sendPubsubPacket(Type.GET, new OptionsExtension(jid, getId(), subscriptionId));
    254 		FormNode ext = (FormNode)packet.getExtension(PubSubElementType.OPTIONS);
    255 		return new SubscribeForm(ext.getForm());
    256 	}
    257 
    258 	/**
    259 	 * Register a listener for item publication events.  This
    260 	 * listener will get called whenever an item is published to
    261 	 * this node.
    262 	 *
    263 	 * @param listener The handler for the event
    264 	 */
    265 	public void addItemEventListener(ItemEventListener listener)
    266 	{
    267 		PacketListener conListener = new ItemEventTranslator(listener);
    268 		itemEventToListenerMap.put(listener, conListener);
    269 		con.addPacketListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));
    270 	}
    271 
    272 	/**
    273 	 * Unregister a listener for publication events.
    274 	 *
    275 	 * @param listener The handler to unregister
    276 	 */
    277 	public void removeItemEventListener(ItemEventListener listener)
    278 	{
    279 		PacketListener conListener = itemEventToListenerMap.remove(listener);
    280 
    281 		if (conListener != null)
    282 			con.removePacketListener(conListener);
    283 	}
    284 
    285 	/**
    286 	 * Register a listener for configuration events.  This listener
    287 	 * will get called whenever the node's configuration changes.
    288 	 *
    289 	 * @param listener The handler for the event
    290 	 */
    291 	public void addConfigurationListener(NodeConfigListener listener)
    292 	{
    293 		PacketListener conListener = new NodeConfigTranslator(listener);
    294 		configEventToListenerMap.put(listener, conListener);
    295 		con.addPacketListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));
    296 	}
    297 
    298 	/**
    299 	 * Unregister a listener for configuration events.
    300 	 *
    301 	 * @param listener The handler to unregister
    302 	 */
    303 	public void removeConfigurationListener(NodeConfigListener listener)
    304 	{
    305 		PacketListener conListener = configEventToListenerMap .remove(listener);
    306 
    307 		if (conListener != null)
    308 			con.removePacketListener(conListener);
    309 	}
    310 
    311 	/**
    312 	 * Register an listener for item delete events.  This listener
    313 	 * gets called whenever an item is deleted from the node.
    314 	 *
    315 	 * @param listener The handler for the event
    316 	 */
    317 	public void addItemDeleteListener(ItemDeleteListener listener)
    318 	{
    319 		PacketListener delListener = new ItemDeleteTranslator(listener);
    320 		itemDeleteToListenerMap.put(listener, delListener);
    321 		EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");
    322 		EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());
    323 
    324 		con.addPacketListener(delListener, new OrFilter(deleteItem, purge));
    325 	}
    326 
    327 	/**
    328 	 * Unregister a listener for item delete events.
    329 	 *
    330 	 * @param listener The handler to unregister
    331 	 */
    332 	public void removeItemDeleteListener(ItemDeleteListener listener)
    333 	{
    334 		PacketListener conListener = itemDeleteToListenerMap .remove(listener);
    335 
    336 		if (conListener != null)
    337 			con.removePacketListener(conListener);
    338 	}
    339 
    340 	@Override
    341 	public String toString()
    342 	{
    343 		return super.toString() + " " + getClass().getName() + " id: " + id;
    344 	}
    345 
    346 	protected PubSub createPubsubPacket(Type type, PacketExtension ext)
    347 	{
    348 		return createPubsubPacket(type, ext, null);
    349 	}
    350 
    351 	protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)
    352 	{
    353 		return PubSubManager.createPubsubPacket(to, type, ext, ns);
    354 	}
    355 
    356 	protected Packet sendPubsubPacket(Type type, NodeExtension ext)
    357 		throws XMPPException
    358 	{
    359 		return PubSubManager.sendPubsubPacket(con, to, type, ext);
    360 	}
    361 
    362 	protected Packet sendPubsubPacket(Type type, NodeExtension ext, PubSubNamespace ns)
    363 		throws XMPPException
    364 	{
    365 		return PubSubManager.sendPubsubPacket(con, to, type, ext, ns);
    366 	}
    367 
    368 
    369 	private static List<String> getSubscriptionIds(Packet packet)
    370 	{
    371 		HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim");
    372 		List<String> values = null;
    373 
    374 		if (headers != null)
    375 		{
    376 			values = new ArrayList<String>(headers.getHeaders().size());
    377 
    378 			for (Header header : headers.getHeaders())
    379 			{
    380 				values.add(header.getValue());
    381 			}
    382 		}
    383 		return values;
    384 	}
    385 
    386 	/**
    387 	 * This class translates low level item publication events into api level objects for
    388 	 * user consumption.
    389 	 *
    390 	 * @author Robin Collier
    391 	 */
    392 	public class ItemEventTranslator implements PacketListener
    393 	{
    394 		private ItemEventListener listener;
    395 
    396 		public ItemEventTranslator(ItemEventListener eventListener)
    397 		{
    398 			listener = eventListener;
    399 		}
    400 
    401 		public void processPacket(Packet packet)
    402 		{
    403 	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
    404 			ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
    405 			DelayInformation delay = (DelayInformation)packet.getExtension("delay", "urn:xmpp:delay");
    406 
    407 			// If there was no delay based on XEP-0203, then try XEP-0091 for backward compatibility
    408 			if (delay == null)
    409 			{
    410 				delay = (DelayInformation)packet.getExtension("x", "jabber:x:delay");
    411 			}
    412 			ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), (delay == null ? null : delay.getStamp()));
    413 			listener.handlePublishedItems(eventItems);
    414 		}
    415 	}
    416 
    417 	/**
    418 	 * This class translates low level item deletion events into api level objects for
    419 	 * user consumption.
    420 	 *
    421 	 * @author Robin Collier
    422 	 */
    423 	public class ItemDeleteTranslator implements PacketListener
    424 	{
    425 		private ItemDeleteListener listener;
    426 
    427 		public ItemDeleteTranslator(ItemDeleteListener eventListener)
    428 		{
    429 			listener = eventListener;
    430 		}
    431 
    432 		public void processPacket(Packet packet)
    433 		{
    434 	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
    435 
    436 	        List<PacketExtension> extList = event.getExtensions();
    437 
    438 	        if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName()))
    439 	        {
    440 	        	listener.handlePurge();
    441 	        }
    442 	        else
    443 	        {
    444 				ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
    445 				Collection<? extends PacketExtension> pubItems = itemsElem.getItems();
    446 				Iterator<RetractItem> it = (Iterator<RetractItem>)pubItems.iterator();
    447 				List<String> items = new ArrayList<String>(pubItems.size());
    448 
    449 				while (it.hasNext())
    450 				{
    451 					RetractItem item = it.next();
    452 					items.add(item.getId());
    453 				}
    454 
    455 				ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));
    456 				listener.handleDeletedItems(eventItems);
    457 	        }
    458 		}
    459 	}
    460 
    461 	/**
    462 	 * This class translates low level node configuration events into api level objects for
    463 	 * user consumption.
    464 	 *
    465 	 * @author Robin Collier
    466 	 */
    467 	public class NodeConfigTranslator implements PacketListener
    468 	{
    469 		private NodeConfigListener listener;
    470 
    471 		public NodeConfigTranslator(NodeConfigListener eventListener)
    472 		{
    473 			listener = eventListener;
    474 		}
    475 
    476 		public void processPacket(Packet packet)
    477 		{
    478 	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
    479 			ConfigurationEvent config = (ConfigurationEvent)event.getEvent();
    480 
    481 			listener.handleNodeConfiguration(config);
    482 		}
    483 	}
    484 
    485 	/**
    486 	 * Filter for {@link PacketListener} to filter out events not specific to the
    487 	 * event type expected for this node.
    488 	 *
    489 	 * @author Robin Collier
    490 	 */
    491 	class EventContentFilter implements PacketFilter
    492 	{
    493 		private String firstElement;
    494 		private String secondElement;
    495 
    496 		EventContentFilter(String elementName)
    497 		{
    498 			firstElement = elementName;
    499 		}
    500 
    501 		EventContentFilter(String firstLevelEelement, String secondLevelElement)
    502 		{
    503 			firstElement = firstLevelEelement;
    504 			secondElement = secondLevelElement;
    505 		}
    506 
    507 		public boolean accept(Packet packet)
    508 		{
    509 			if (!(packet instanceof Message))
    510 				return false;
    511 
    512 			EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
    513 
    514 			if (event == null)
    515 				return false;
    516 
    517 			NodeExtension embedEvent = event.getEvent();
    518 
    519 			if (embedEvent == null)
    520 				return false;
    521 
    522 			if (embedEvent.getElementName().equals(firstElement))
    523 			{
    524 				if (!embedEvent.getNode().equals(getId()))
    525 					return false;
    526 
    527 				if (secondElement == null)
    528 					return true;
    529 
    530 				if (embedEvent instanceof EmbeddedPacketExtension)
    531 				{
    532 					List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions();
    533 
    534 					if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))
    535 						return true;
    536 				}
    537 			}
    538 			return false;
    539 		}
    540 	}
    541 }
    542