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.StanzaListener; 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.StanzaFilter; 031import org.jivesoftware.smack.packet.Message; 032import org.jivesoftware.smack.packet.Stanza; 033import org.jivesoftware.smack.packet.ExtensionElement; 034import org.jivesoftware.smack.packet.IQ.Type; 035import org.jivesoftware.smackx.delay.DelayInformationManager; 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>, StanzaListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, StanzaListener>(); 054 protected ConcurrentHashMap<ItemDeleteListener, StanzaListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, StanzaListener>(); 055 protected ConcurrentHashMap<NodeConfigListener, StanzaListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, StanzaListener>(); 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 Stanza 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 stanza(/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<ExtensionElement> additionalExtensions, Collection<ExtensionElement> returnedExtensions) 168 throws NoResponseException, XMPPErrorException, NotConnectedException { 169 return getSubscriptions(additionalExtensions, returnedExtensions, null); 170 } 171 172 /** 173 * Get the subscriptions currently associated with this node as owner. 174 * 175 * @return List of {@link Subscription} 176 * @throws XMPPErrorException 177 * @throws NoResponseException 178 * @throws NotConnectedException 179 * @see #getSubscriptionsAsOwner(List, Collection) 180 * @since 4.1 181 */ 182 public List<Subscription> getSubscriptionsAsOwner() throws NoResponseException, XMPPErrorException, 183 NotConnectedException { 184 return getSubscriptionsAsOwner(null, null); 185 } 186 187 /** 188 * Get the subscriptions currently associated with this node as owner. 189 * <p> 190 * Unlike {@link #getSubscriptions(List, Collection)}, which only retrieves the subscriptions of the current entity 191 * ("user"), this method returns a list of <b>all</b> subscriptions. This requires the entity to have the sufficient 192 * privileges to manage subscriptions. 193 * </p> 194 * <p> 195 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 196 * {@code returnedExtensions} will be filled with the stanza(/packet) extensions found in the answer. 197 * </p> 198 * 199 * @param additionalExtensions 200 * @param returnedExtensions a collection that will be filled with the returned stanza(/packet) extensions 201 * @return List of {@link Subscription} 202 * @throws NoResponseException 203 * @throws XMPPErrorException 204 * @throws NotConnectedException 205 * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-subscriptions-retrieve">XEP-60 ยง 8.8.1 - 206 * Retrieve Subscriptions List</a> 207 * @since 4.1 208 */ 209 public List<Subscription> getSubscriptionsAsOwner(List<ExtensionElement> additionalExtensions, 210 Collection<ExtensionElement> returnedExtensions) throws NoResponseException, XMPPErrorException, 211 NotConnectedException { 212 return getSubscriptions(additionalExtensions, returnedExtensions, PubSubNamespace.OWNER); 213 } 214 215 private List<Subscription> getSubscriptions(List<ExtensionElement> additionalExtensions, 216 Collection<ExtensionElement> returnedExtensions, PubSubNamespace pubSubNamespace) 217 throws NoResponseException, XMPPErrorException, NotConnectedException { 218 PubSub pubSub = createPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()), pubSubNamespace); 219 if (additionalExtensions != null) { 220 for (ExtensionElement pe : additionalExtensions) { 221 pubSub.addExtension(pe); 222 } 223 } 224 PubSub reply = sendPubsubPacket(pubSub); 225 if (returnedExtensions != null) { 226 returnedExtensions.addAll(reply.getExtensions()); 227 } 228 SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtension(PubSubElementType.SUBSCRIPTIONS); 229 return subElem.getSubscriptions(); 230 } 231 232 /** 233 * Get the affiliations of this node. 234 * 235 * @return List of {@link Affiliation} 236 * @throws NoResponseException 237 * @throws XMPPErrorException 238 * @throws NotConnectedException 239 */ 240 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, 241 NotConnectedException { 242 return getAffiliations(null, null); 243 } 244 245 /** 246 * Get the affiliations of this node. 247 * <p> 248 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 249 * {@code returnedExtensions} will be filled with the stanza(/packet) extensions found in the answer. 250 * </p> 251 * 252 * @param additionalExtensions additional {@code PacketExtensions} add to the request 253 * @param returnedExtensions a collection that will be filled with the returned packet 254 * extensions 255 * @return List of {@link Affiliation} 256 * @throws NoResponseException 257 * @throws XMPPErrorException 258 * @throws NotConnectedException 259 */ 260 public List<Affiliation> getAffiliations(List<ExtensionElement> additionalExtensions, Collection<ExtensionElement> returnedExtensions) 261 throws NoResponseException, XMPPErrorException, NotConnectedException { 262 PubSub pubSub = createPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS, getId())); 263 if (additionalExtensions != null) { 264 for (ExtensionElement pe : additionalExtensions) { 265 pubSub.addExtension(pe); 266 } 267 } 268 PubSub reply = sendPubsubPacket(pubSub); 269 if (returnedExtensions != null) { 270 returnedExtensions.addAll(reply.getExtensions()); 271 } 272 AffiliationsExtension affilElem = (AffiliationsExtension) reply.getExtension(PubSubElementType.AFFILIATIONS); 273 return affilElem.getAffiliations(); 274 } 275 276 /** 277 * The user subscribes to the node using the supplied jid. The 278 * bare jid portion of this one must match the jid for the connection. 279 * 280 * Please note that the {@link Subscription.State} should be checked 281 * on return since more actions may be required by the caller. 282 * {@link Subscription.State#pending} - The owner must approve the subscription 283 * request before messages will be received. 284 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 285 * the caller must configure the subscription before messages will be received. If it is false 286 * the caller can configure it but is not required to do so. 287 * @param jid The jid to subscribe as. 288 * @return The subscription 289 * @throws XMPPErrorException 290 * @throws NoResponseException 291 * @throws NotConnectedException 292 */ 293 public Subscription subscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 294 { 295 PubSub pubSub = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); 296 PubSub reply = sendPubsubPacket(pubSub); 297 return reply.getExtension(PubSubElementType.SUBSCRIPTION); 298 } 299 300 /** 301 * The user subscribes to the node using the supplied jid and subscription 302 * options. The bare jid portion of this one must match the jid for the 303 * connection. 304 * 305 * Please note that the {@link Subscription.State} should be checked 306 * on return since more actions may be required by the caller. 307 * {@link Subscription.State#pending} - The owner must approve the subscription 308 * request before messages will be received. 309 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 310 * the caller must configure the subscription before messages will be received. If it is false 311 * the caller can configure it but is not required to do so. 312 * @param jid The jid to subscribe as. 313 * @return The subscription 314 * @throws XMPPErrorException 315 * @throws NoResponseException 316 * @throws NotConnectedException 317 */ 318 public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException 319 { 320 PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); 321 request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); 322 PubSub reply = PubSubManager.sendPubsubPacket(con, request); 323 return reply.getExtension(PubSubElementType.SUBSCRIPTION); 324 } 325 326 /** 327 * Remove the subscription related to the specified JID. This will only 328 * work if there is only 1 subscription. If there are multiple subscriptions, 329 * use {@link #unsubscribe(String, String)}. 330 * 331 * @param jid The JID used to subscribe to the node 332 * @throws XMPPErrorException 333 * @throws NoResponseException 334 * @throws NotConnectedException 335 * 336 */ 337 public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 338 { 339 unsubscribe(jid, null); 340 } 341 342 /** 343 * Remove the specific subscription related to the specified JID. 344 * 345 * @param jid The JID used to subscribe to the node 346 * @param subscriptionId The id of the subscription being removed 347 * @throws XMPPErrorException 348 * @throws NoResponseException 349 * @throws NotConnectedException 350 */ 351 public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException 352 { 353 sendPubsubPacket(createPubsubPacket(Type.set, new UnsubscribeExtension(jid, getId(), subscriptionId))); 354 } 355 356 /** 357 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted 358 * via the {@link #sendConfigurationForm(Form)}. 359 * 360 * @return A subscription options form 361 * @throws XMPPErrorException 362 * @throws NoResponseException 363 * @throws NotConnectedException 364 */ 365 public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException 366 { 367 return getSubscriptionOptions(jid, null); 368 } 369 370 371 /** 372 * Get the options for configuring the specified subscription. 373 * 374 * @param jid JID the subscription is registered under 375 * @param subscriptionId The subscription id 376 * 377 * @return The subscription option form 378 * @throws XMPPErrorException 379 * @throws NoResponseException 380 * @throws NotConnectedException 381 * 382 */ 383 public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException 384 { 385 PubSub packet = sendPubsubPacket(createPubsubPacket(Type.get, new OptionsExtension(jid, getId(), subscriptionId))); 386 FormNode ext = packet.getExtension(PubSubElementType.OPTIONS); 387 return new SubscribeForm(ext.getForm()); 388 } 389 390 /** 391 * Register a listener for item publication events. This 392 * listener will get called whenever an item is published to 393 * this node. 394 * 395 * @param listener The handler for the event 396 */ 397 @SuppressWarnings("unchecked") 398 public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 399 { 400 StanzaListener conListener = new ItemEventTranslator(listener); 401 itemEventToListenerMap.put(listener, conListener); 402 con.addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); 403 } 404 405 /** 406 * Unregister a listener for publication events. 407 * 408 * @param listener The handler to unregister 409 */ 410 public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 411 { 412 StanzaListener conListener = itemEventToListenerMap.remove(listener); 413 414 if (conListener != null) 415 con.removeSyncStanzaListener(conListener); 416 } 417 418 /** 419 * Register a listener for configuration events. This listener 420 * will get called whenever the node's configuration changes. 421 * 422 * @param listener The handler for the event 423 */ 424 public void addConfigurationListener(NodeConfigListener listener) 425 { 426 StanzaListener conListener = new NodeConfigTranslator(listener); 427 configEventToListenerMap.put(listener, conListener); 428 con.addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); 429 } 430 431 /** 432 * Unregister a listener for configuration events. 433 * 434 * @param listener The handler to unregister 435 */ 436 public void removeConfigurationListener(NodeConfigListener listener) 437 { 438 StanzaListener conListener = configEventToListenerMap .remove(listener); 439 440 if (conListener != null) 441 con.removeSyncStanzaListener(conListener); 442 } 443 444 /** 445 * Register an listener for item delete events. This listener 446 * gets called whenever an item is deleted from the node. 447 * 448 * @param listener The handler for the event 449 */ 450 public void addItemDeleteListener(ItemDeleteListener listener) 451 { 452 StanzaListener delListener = new ItemDeleteTranslator(listener); 453 itemDeleteToListenerMap.put(listener, delListener); 454 EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract"); 455 EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString()); 456 457 con.addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge)); 458 } 459 460 /** 461 * Unregister a listener for item delete events. 462 * 463 * @param listener The handler to unregister 464 */ 465 public void removeItemDeleteListener(ItemDeleteListener listener) 466 { 467 StanzaListener conListener = itemDeleteToListenerMap .remove(listener); 468 469 if (conListener != null) 470 con.removeSyncStanzaListener(conListener); 471 } 472 473 @Override 474 public String toString() 475 { 476 return super.toString() + " " + getClass().getName() + " id: " + id; 477 } 478 479 protected PubSub createPubsubPacket(Type type, ExtensionElement ext) 480 { 481 return createPubsubPacket(type, ext, null); 482 } 483 484 protected PubSub createPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 485 { 486 return PubSub.createPubsubPacket(to, type, ext, ns); 487 } 488 489 protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException 490 { 491 return PubSubManager.sendPubsubPacket(con, packet); 492 } 493 494 495 private static List<String> getSubscriptionIds(Stanza packet) 496 { 497 HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim"); 498 List<String> values = null; 499 500 if (headers != null) 501 { 502 values = new ArrayList<String>(headers.getHeaders().size()); 503 504 for (Header header : headers.getHeaders()) 505 { 506 values.add(header.getValue()); 507 } 508 } 509 return values; 510 } 511 512 /** 513 * This class translates low level item publication events into api level objects for 514 * user consumption. 515 * 516 * @author Robin Collier 517 */ 518 public class ItemEventTranslator implements StanzaListener 519 { 520 @SuppressWarnings("rawtypes") 521 private ItemEventListener listener; 522 523 public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener) 524 { 525 listener = eventListener; 526 } 527 528 @SuppressWarnings({ "rawtypes", "unchecked" }) 529 public void processPacket(Stanza packet) 530 { 531 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 532 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 533 ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), DelayInformationManager.getDelayTimestamp(packet)); 534 listener.handlePublishedItems(eventItems); 535 } 536 } 537 538 /** 539 * This class translates low level item deletion events into api level objects for 540 * user consumption. 541 * 542 * @author Robin Collier 543 */ 544 public class ItemDeleteTranslator implements StanzaListener 545 { 546 private ItemDeleteListener listener; 547 548 public ItemDeleteTranslator(ItemDeleteListener eventListener) 549 { 550 listener = eventListener; 551 } 552 553 public void processPacket(Stanza packet) 554 { 555 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 556 557 List<ExtensionElement> extList = event.getExtensions(); 558 559 if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName())) 560 { 561 listener.handlePurge(); 562 } 563 else 564 { 565 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 566 @SuppressWarnings("unchecked") 567 Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems(); 568 List<String> items = new ArrayList<String>(pubItems.size()); 569 570 for (RetractItem item : pubItems) 571 { 572 items.add(item.getId()); 573 } 574 575 ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet)); 576 listener.handleDeletedItems(eventItems); 577 } 578 } 579 } 580 581 /** 582 * This class translates low level node configuration events into api level objects for 583 * user consumption. 584 * 585 * @author Robin Collier 586 */ 587 public class NodeConfigTranslator implements StanzaListener 588 { 589 private NodeConfigListener listener; 590 591 public NodeConfigTranslator(NodeConfigListener eventListener) 592 { 593 listener = eventListener; 594 } 595 596 public void processPacket(Stanza packet) 597 { 598 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 599 ConfigurationEvent config = (ConfigurationEvent)event.getEvent(); 600 601 listener.handleNodeConfiguration(config); 602 } 603 } 604 605 /** 606 * Filter for {@link StanzaListener} to filter out events not specific to the 607 * event type expected for this node. 608 * 609 * @author Robin Collier 610 */ 611 class EventContentFilter implements StanzaFilter 612 { 613 private String firstElement; 614 private String secondElement; 615 616 EventContentFilter(String elementName) 617 { 618 firstElement = elementName; 619 } 620 621 EventContentFilter(String firstLevelEelement, String secondLevelElement) 622 { 623 firstElement = firstLevelEelement; 624 secondElement = secondLevelElement; 625 } 626 627 public boolean accept(Stanza packet) 628 { 629 if (!(packet instanceof Message)) 630 return false; 631 632 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 633 634 if (event == null) 635 return false; 636 637 NodeExtension embedEvent = event.getEvent(); 638 639 if (embedEvent == null) 640 return false; 641 642 if (embedEvent.getElementName().equals(firstElement)) 643 { 644 if (!embedEvent.getNode().equals(getId())) 645 return false; 646 647 if (secondElement == null) 648 return true; 649 650 if (embedEvent instanceof EmbeddedPacketExtension) 651 { 652 List<ExtensionElement> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions(); 653 654 if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement)) 655 return true; 656 } 657 } 658 return false; 659 } 660 } 661}