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 */
20package org.jivesoftware.smackx.pubsub;
21
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.Iterator;
25import java.util.List;
26import java.util.concurrent.ConcurrentHashMap;
27
28import org.jivesoftware.smack.PacketListener;
29import org.jivesoftware.smack.Connection;
30import org.jivesoftware.smack.XMPPException;
31import org.jivesoftware.smack.filter.OrFilter;
32import org.jivesoftware.smack.filter.PacketFilter;
33import org.jivesoftware.smack.packet.Message;
34import org.jivesoftware.smack.packet.Packet;
35import org.jivesoftware.smack.packet.PacketExtension;
36import org.jivesoftware.smack.packet.IQ.Type;
37import org.jivesoftware.smackx.Form;
38import org.jivesoftware.smackx.packet.DelayInformation;
39import org.jivesoftware.smackx.packet.DiscoverInfo;
40import org.jivesoftware.smackx.packet.Header;
41import org.jivesoftware.smackx.packet.HeadersExtension;
42import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
43import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
44import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
45import org.jivesoftware.smackx.pubsub.packet.PubSub;
46import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
47import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
48import org.jivesoftware.smackx.pubsub.util.NodeUtils;
49
50abstract 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