• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 	 */
Node(Connection connection, String nodeName)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 	 */
setTo(String toAddress)79 	void setTo(String toAddress)
80 	{
81 		to = toAddress;
82 	}
83 
84 	/**
85 	 * Get the NodeId
86 	 *
87 	 * @return the node id
88 	 */
getId()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 	 */
getNodeConfiguration()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 	 */
sendConfigurationForm(Form submitForm)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 	 */
discoverInfo()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 	 */
getSubscriptions()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 	 */
subscribe(String jid)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 	 */
subscribe(String jid, SubscribeForm subForm)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 	 */
unsubscribe(String jid)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 	 */
unsubscribe(String jid, String subscriptionId)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 	 */
getSubscriptionOptions(String jid)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 	 */
getSubscriptionOptions(String jid, String subscriptionId)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 	 */
addItemEventListener(ItemEventListener listener)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 	 */
removeItemEventListener(ItemEventListener listener)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 	 */
addConfigurationListener(NodeConfigListener listener)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 	 */
removeConfigurationListener(NodeConfigListener listener)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 	 */
addItemDeleteListener(ItemDeleteListener listener)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 	 */
removeItemDeleteListener(ItemDeleteListener listener)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
toString()341 	public String toString()
342 	{
343 		return super.toString() + " " + getClass().getName() + " id: " + id;
344 	}
345 
createPubsubPacket(Type type, PacketExtension ext)346 	protected PubSub createPubsubPacket(Type type, PacketExtension ext)
347 	{
348 		return createPubsubPacket(type, ext, null);
349 	}
350 
createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)351 	protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)
352 	{
353 		return PubSubManager.createPubsubPacket(to, type, ext, ns);
354 	}
355 
sendPubsubPacket(Type type, NodeExtension ext)356 	protected Packet sendPubsubPacket(Type type, NodeExtension ext)
357 		throws XMPPException
358 	{
359 		return PubSubManager.sendPubsubPacket(con, to, type, ext);
360 	}
361 
sendPubsubPacket(Type type, NodeExtension ext, PubSubNamespace ns)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 
getSubscriptionIds(Packet packet)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 
ItemEventTranslator(ItemEventListener eventListener)396 		public ItemEventTranslator(ItemEventListener eventListener)
397 		{
398 			listener = eventListener;
399 		}
400 
processPacket(Packet packet)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 
ItemDeleteTranslator(ItemDeleteListener eventListener)427 		public ItemDeleteTranslator(ItemDeleteListener eventListener)
428 		{
429 			listener = eventListener;
430 		}
431 
processPacket(Packet packet)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 
NodeConfigTranslator(NodeConfigListener eventListener)471 		public NodeConfigTranslator(NodeConfigListener eventListener)
472 		{
473 			listener = eventListener;
474 		}
475 
processPacket(Packet packet)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 
EventContentFilter(String elementName)496 		EventContentFilter(String elementName)
497 		{
498 			firstElement = elementName;
499 		}
500 
EventContentFilter(String firstLevelEelement, String secondLevelElement)501 		EventContentFilter(String firstLevelEelement, String secondLevelElement)
502 		{
503 			firstElement = firstLevelEelement;
504 			secondElement = secondLevelElement;
505 		}
506 
accept(Packet packet)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