blob: 1b0ff5afd24b8d7e0d4e8b093f5afe4920bc1a78 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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}