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