001/** 002 * 003 * Copyright 2009 Robin Collier. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.pubsub; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.List; 022import java.util.concurrent.ConcurrentHashMap; 023 024import org.jivesoftware.smack.PacketListener; 025import org.jivesoftware.smack.SmackException.NoResponseException; 026import org.jivesoftware.smack.SmackException.NotConnectedException; 027import org.jivesoftware.smack.XMPPConnection; 028import org.jivesoftware.smack.XMPPException.XMPPErrorException; 029import org.jivesoftware.smack.filter.OrFilter; 030import org.jivesoftware.smack.filter.PacketFilter; 031import org.jivesoftware.smack.packet.Message; 032import org.jivesoftware.smack.packet.Packet; 033import org.jivesoftware.smack.packet.PacketExtension; 034import org.jivesoftware.smack.packet.IQ.Type; 035import org.jivesoftware.smackx.delay.packet.DelayInformation; 036import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 037import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener; 038import org.jivesoftware.smackx.pubsub.listener.ItemEventListener; 039import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener; 040import org.jivesoftware.smackx.pubsub.packet.PubSub; 041import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; 042import org.jivesoftware.smackx.pubsub.util.NodeUtils; 043import org.jivesoftware.smackx.shim.packet.Header; 044import org.jivesoftware.smackx.shim.packet.HeadersExtension; 045import org.jivesoftware.smackx.xdata.Form; 046 047abstract public class Node 048{ 049 protected XMPPConnection con; 050 protected String id; 051 protected String to; 052 053 protected ConcurrentHashMap<ItemEventListener<Item>, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, PacketListener>(); 054 protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>(); 055 protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>(); 056 057 /** 058 * Construct a node associated to the supplied connection with the specified 059 * node id. 060 * 061 * @param connection The connection the node is associated with 062 * @param nodeName The node id 063 */ 064 Node(XMPPConnection connection, String nodeName) 065 { 066 con = connection; 067 id = nodeName; 068 } 069 070 /** 071 * Some XMPP servers may require a specific service to be addressed on the 072 * server. 073 * 074 * For example, OpenFire requires the server to be prefixed by <b>pubsub</b> 075 */ 076 void setTo(String toAddress) 077 { 078 to = toAddress; 079 } 080 081 /** 082 * Get the NodeId 083 * 084 * @return the node id 085 */ 086 public String getId() 087 { 088 return id; 089 } 090 /** 091 * Returns a configuration form, from which you can create an answer form to be submitted 092 * via the {@link #sendConfigurationForm(Form)}. 093 * 094 * @return the configuration form 095 * @throws XMPPErrorException 096 * @throws NoResponseException 097 * @throws NotConnectedException 098 */ 099 public ConfigureForm getNodeConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException 100 { 101 PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension( 102 PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER); 103 Packet reply = sendPubsubPacket(pubSub); 104 return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER); 105 } 106 107 /** 108 * Update the configuration with the contents of the new {@link Form} 109 * 110 * @param submitForm 111 * @throws XMPPErrorException 112 * @throws NoResponseException 113 * @throws NotConnectedException 114 */ 115 public void sendConfigurationForm(Form submitForm) throws NoResponseException, XMPPErrorException, NotConnectedException 116 { 117 PubSub packet = createPubsubPacket(Type.SET, new FormNode(FormNodeType.CONFIGURE_OWNER, 118 getId(), submitForm), PubSubNamespace.OWNER); 119 con.createPacketCollectorAndSend(packet).nextResultOrThrow(); 120 } 121 122 /** 123 * Discover node information in standard {@link DiscoverInfo} format. 124 * 125 * @return The discovery information about the node. 126 * @throws XMPPErrorException 127 * @throws NoResponseException if there was no response from the server. 128 * @throws NotConnectedException 129 */ 130 public DiscoverInfo discoverInfo() throws NoResponseException, XMPPErrorException, NotConnectedException 131 { 132 DiscoverInfo info = new DiscoverInfo(); 133 info.setTo(to); 134 info.setNode(getId()); 135 return (DiscoverInfo) con.createPacketCollectorAndSend(info).nextResultOrThrow(); 136 } 137 138 /** 139 * Get the subscriptions currently associated with this node. 140 * 141 * @return List of {@link Subscription} 142 * @throws XMPPErrorException 143 * @throws NoResponseException 144 * @throws NotConnectedException 145 * 146 */ 147 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException 148 { 149 return getSubscriptions(null, null); 150 } 151 152 /** 153 * Get the subscriptions currently associated with this node. 154 * <p> 155 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 156 * {@code returnedExtensions} will be filled with the packet extensions found in the answer. 157 * </p> 158 * 159 * @param additionalExtensions 160 * @param returnedExtensions a collection that will be filled with the returned packet 161 * extensions 162 * @return List of {@link Subscription} 163 * @throws NoResponseException 164 * @throws XMPPErrorException 165 * @throws NotConnectedException 166 */ 167 public List<Subscription> getSubscriptions(List<PacketExtension> additionalExtensions, Collection<PacketExtension> returnedExtensions) 168 throws NoResponseException, XMPPErrorException, NotConnectedException { 169 PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension( 170 PubSubElementType.SUBSCRIPTIONS, getId())); 171 if (additionalExtensions != null) { 172 for (PacketExtension pe : additionalExtensions) { 173 pubSub.addExtension(pe); 174 } 175 } 176 PubSub reply = (PubSub) sendPubsubPacket(pubSub); 177 if (returnedExtensions != null) { 178 returnedExtensions.addAll(reply.getExtensions()); 179 } 180 SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtension(PubSubElementType.SUBSCRIPTIONS); 181 return subElem.getSubscriptions(); 182 } 183 184 /** 185 * Get the affiliations of this node. 186 * 187 * @return List of {@link Affiliation} 188 * @throws NoResponseException 189 * @throws XMPPErrorException 190 * @throws NotConnectedException 191 */ 192 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, 193 NotConnectedException { 194 return getAffiliations(null, null); 195 } 196 197 /** 198 * Get the affiliations of this node. 199 * <p> 200 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 201 * {@code returnedExtensions} will be filled with the packet extensions found in the answer. 202 * </p> 203 * 204 * @param additionalExtensions additional {@code PacketExtensions} add to the request 205 * @param returnedExtensions a collection that will be filled with the returned packet 206 * extensions 207 * @return List of {@link Affiliation} 208 * @throws NoResponseException 209 * @throws XMPPErrorException 210 * @throws NotConnectedException 211 */ 212 public List<Affiliation> getAffiliations(List<PacketExtension> additionalExtensions, Collection<PacketExtension> returnedExtensions) 213 throws NoResponseException, XMPPErrorException, NotConnectedException { 214 PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.AFFILIATIONS, getId())); 215 if (additionalExtensions != null) { 216 for (PacketExtension pe : additionalExtensions) { 217 pubSub.addExtension(pe); 218 } 219 } 220 PubSub reply = (PubSub) sendPubsubPacket(pubSub); 221 if (returnedExtensions != null) { 222 returnedExtensions.addAll(reply.getExtensions()); 223 } 224 AffiliationsExtension affilElem = (AffiliationsExtension) reply.getExtension(PubSubElementType.AFFILIATIONS); 225 return affilElem.getAffiliations(); 226 } 227 228 /** 229 * The user subscribes to the node using the supplied jid. The 230 * bare jid portion of this one must match the jid for the connection. 231 * 232 * Please note that the {@link Subscription.State} should be checked 233 * on return since more actions may be required by the caller. 234 * {@link Subscription.State#pending} - The owner must approve the subscription 235 * request before messages will be received. 236 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 237 * the caller must configure the subscription before messages will be received. If it is false 238 * the caller can configure it but is not required to do so. 239 * @param jid The jid to subscribe as. 240 * @return The subscription 241 * @throws XMPPErrorException 242 * @throws NoResponseException 243 * @throws NotConnectedException 244 */ 245 public Subscription subscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 246 { 247 PubSub pubSub = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId())); 248 PubSub reply = (PubSub)sendPubsubPacket(pubSub); 249 return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION); 250 } 251 252 /** 253 * The user subscribes to the node using the supplied jid and subscription 254 * options. The bare jid portion of this one must match the jid for the 255 * connection. 256 * 257 * Please note that the {@link Subscription.State} should be checked 258 * on return since more actions may be required by the caller. 259 * {@link Subscription.State#pending} - The owner must approve the subscription 260 * request before messages will be received. 261 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 262 * the caller must configure the subscription before messages will be received. If it is false 263 * the caller can configure it but is not required to do so. 264 * @param jid The jid to subscribe as. 265 * @return The subscription 266 * @throws XMPPErrorException 267 * @throws NoResponseException 268 * @throws NotConnectedException 269 */ 270 public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException 271 { 272 PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId())); 273 request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); 274 PubSub reply = (PubSub)PubSubManager.sendPubsubPacket(con, request); 275 return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION); 276 } 277 278 /** 279 * Remove the subscription related to the specified JID. This will only 280 * work if there is only 1 subscription. If there are multiple subscriptions, 281 * use {@link #unsubscribe(String, String)}. 282 * 283 * @param jid The JID used to subscribe to the node 284 * @throws XMPPErrorException 285 * @throws NoResponseException 286 * @throws NotConnectedException 287 * 288 */ 289 public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 290 { 291 unsubscribe(jid, null); 292 } 293 294 /** 295 * Remove the specific subscription related to the specified JID. 296 * 297 * @param jid The JID used to subscribe to the node 298 * @param subscriptionId The id of the subscription being removed 299 * @throws XMPPErrorException 300 * @throws NoResponseException 301 * @throws NotConnectedException 302 */ 303 public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException 304 { 305 sendPubsubPacket(createPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(), subscriptionId))); 306 } 307 308 /** 309 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted 310 * via the {@link #sendConfigurationForm(Form)}. 311 * 312 * @return A subscription options form 313 * @throws XMPPErrorException 314 * @throws NoResponseException 315 * @throws NotConnectedException 316 */ 317 public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 318 { 319 return getSubscriptionOptions(jid, null); 320 } 321 322 323 /** 324 * Get the options for configuring the specified subscription. 325 * 326 * @param jid JID the subscription is registered under 327 * @param subscriptionId The subscription id 328 * 329 * @return The subscription option form 330 * @throws XMPPErrorException 331 * @throws NoResponseException 332 * @throws NotConnectedException 333 * 334 */ 335 public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException 336 { 337 PubSub packet = (PubSub)sendPubsubPacket(createPubsubPacket(Type.GET, new OptionsExtension(jid, getId(), subscriptionId))); 338 FormNode ext = (FormNode)packet.getExtension(PubSubElementType.OPTIONS); 339 return new SubscribeForm(ext.getForm()); 340 } 341 342 /** 343 * Register a listener for item publication events. This 344 * listener will get called whenever an item is published to 345 * this node. 346 * 347 * @param listener The handler for the event 348 */ 349 @SuppressWarnings("unchecked") 350 public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 351 { 352 PacketListener conListener = new ItemEventTranslator(listener); 353 itemEventToListenerMap.put(listener, conListener); 354 con.addPacketListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); 355 } 356 357 /** 358 * Unregister a listener for publication events. 359 * 360 * @param listener The handler to unregister 361 */ 362 public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 363 { 364 PacketListener conListener = itemEventToListenerMap.remove(listener); 365 366 if (conListener != null) 367 con.removePacketListener(conListener); 368 } 369 370 /** 371 * Register a listener for configuration events. This listener 372 * will get called whenever the node's configuration changes. 373 * 374 * @param listener The handler for the event 375 */ 376 public void addConfigurationListener(NodeConfigListener listener) 377 { 378 PacketListener conListener = new NodeConfigTranslator(listener); 379 configEventToListenerMap.put(listener, conListener); 380 con.addPacketListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); 381 } 382 383 /** 384 * Unregister a listener for configuration events. 385 * 386 * @param listener The handler to unregister 387 */ 388 public void removeConfigurationListener(NodeConfigListener listener) 389 { 390 PacketListener conListener = configEventToListenerMap .remove(listener); 391 392 if (conListener != null) 393 con.removePacketListener(conListener); 394 } 395 396 /** 397 * Register an listener for item delete events. This listener 398 * gets called whenever an item is deleted from the node. 399 * 400 * @param listener The handler for the event 401 */ 402 public void addItemDeleteListener(ItemDeleteListener listener) 403 { 404 PacketListener delListener = new ItemDeleteTranslator(listener); 405 itemDeleteToListenerMap.put(listener, delListener); 406 EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract"); 407 EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString()); 408 409 con.addPacketListener(delListener, new OrFilter(deleteItem, purge)); 410 } 411 412 /** 413 * Unregister a listener for item delete events. 414 * 415 * @param listener The handler to unregister 416 */ 417 public void removeItemDeleteListener(ItemDeleteListener listener) 418 { 419 PacketListener conListener = itemDeleteToListenerMap .remove(listener); 420 421 if (conListener != null) 422 con.removePacketListener(conListener); 423 } 424 425 @Override 426 public String toString() 427 { 428 return super.toString() + " " + getClass().getName() + " id: " + id; 429 } 430 431 protected PubSub createPubsubPacket(Type type, PacketExtension ext) 432 { 433 return createPubsubPacket(type, ext, null); 434 } 435 436 protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns) 437 { 438 return PubSub.createPubsubPacket(to, type, ext, ns); 439 } 440 441 protected Packet sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException 442 { 443 return PubSubManager.sendPubsubPacket(con, packet); 444 } 445 446 447 private static List<String> getSubscriptionIds(Packet packet) 448 { 449 HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim"); 450 List<String> values = null; 451 452 if (headers != null) 453 { 454 values = new ArrayList<String>(headers.getHeaders().size()); 455 456 for (Header header : headers.getHeaders()) 457 { 458 values.add(header.getValue()); 459 } 460 } 461 return values; 462 } 463 464 /** 465 * This class translates low level item publication events into api level objects for 466 * user consumption. 467 * 468 * @author Robin Collier 469 */ 470 public class ItemEventTranslator implements PacketListener 471 { 472 @SuppressWarnings("rawtypes") 473 private ItemEventListener listener; 474 475 public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener) 476 { 477 listener = eventListener; 478 } 479 480 @SuppressWarnings({ "rawtypes", "unchecked" }) 481 public void processPacket(Packet packet) 482 { 483 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 484 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 485 DelayInformation delay = (DelayInformation)packet.getExtension("delay", "urn:xmpp:delay"); 486 487 // If there was no delay based on XEP-0203, then try XEP-0091 for backward compatibility 488 if (delay == null) 489 { 490 delay = (DelayInformation)packet.getExtension("x", "jabber:x:delay"); 491 } 492 ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), (delay == null ? null : delay.getStamp())); 493 listener.handlePublishedItems(eventItems); 494 } 495 } 496 497 /** 498 * This class translates low level item deletion events into api level objects for 499 * user consumption. 500 * 501 * @author Robin Collier 502 */ 503 public class ItemDeleteTranslator implements PacketListener 504 { 505 private ItemDeleteListener listener; 506 507 public ItemDeleteTranslator(ItemDeleteListener eventListener) 508 { 509 listener = eventListener; 510 } 511 512 public void processPacket(Packet packet) 513 { 514 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 515 516 List<PacketExtension> extList = event.getExtensions(); 517 518 if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName())) 519 { 520 listener.handlePurge(); 521 } 522 else 523 { 524 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 525 @SuppressWarnings("unchecked") 526 Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems(); 527 List<String> items = new ArrayList<String>(pubItems.size()); 528 529 for (RetractItem item : pubItems) 530 { 531 items.add(item.getId()); 532 } 533 534 ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet)); 535 listener.handleDeletedItems(eventItems); 536 } 537 } 538 } 539 540 /** 541 * This class translates low level node configuration events into api level objects for 542 * user consumption. 543 * 544 * @author Robin Collier 545 */ 546 public class NodeConfigTranslator implements PacketListener 547 { 548 private NodeConfigListener listener; 549 550 public NodeConfigTranslator(NodeConfigListener eventListener) 551 { 552 listener = eventListener; 553 } 554 555 public void processPacket(Packet packet) 556 { 557 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 558 ConfigurationEvent config = (ConfigurationEvent)event.getEvent(); 559 560 listener.handleNodeConfiguration(config); 561 } 562 } 563 564 /** 565 * Filter for {@link PacketListener} to filter out events not specific to the 566 * event type expected for this node. 567 * 568 * @author Robin Collier 569 */ 570 class EventContentFilter implements PacketFilter 571 { 572 private String firstElement; 573 private String secondElement; 574 575 EventContentFilter(String elementName) 576 { 577 firstElement = elementName; 578 } 579 580 EventContentFilter(String firstLevelEelement, String secondLevelElement) 581 { 582 firstElement = firstLevelEelement; 583 secondElement = secondLevelElement; 584 } 585 586 public boolean accept(Packet packet) 587 { 588 if (!(packet instanceof Message)) 589 return false; 590 591 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 592 593 if (event == null) 594 return false; 595 596 NodeExtension embedEvent = event.getEvent(); 597 598 if (embedEvent == null) 599 return false; 600 601 if (embedEvent.getElementName().equals(firstElement)) 602 { 603 if (!embedEvent.getNode().equals(getId())) 604 return false; 605 606 if (secondElement == null) 607 return true; 608 609 if (embedEvent instanceof EmbeddedPacketExtension) 610 { 611 List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions(); 612 613 if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement)) 614 return true; 615 } 616 } 617 return false; 618 } 619 } 620}