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.SmackException.NoResponseException; 025import org.jivesoftware.smack.SmackException.NotConnectedException; 026import org.jivesoftware.smack.StanzaListener; 027import org.jivesoftware.smack.XMPPException.XMPPErrorException; 028import org.jivesoftware.smack.filter.FlexibleStanzaTypeFilter; 029import org.jivesoftware.smack.filter.OrFilter; 030import org.jivesoftware.smack.packet.ExtensionElement; 031import org.jivesoftware.smack.packet.IQ.Type; 032import org.jivesoftware.smack.packet.Message; 033import org.jivesoftware.smack.packet.Stanza; 034 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 final PubSubManager pubSubManager; 050 protected final String id; 051 052 protected ConcurrentHashMap<ItemEventListener<Item>, StanzaListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, StanzaListener>(); 053 protected ConcurrentHashMap<ItemDeleteListener, StanzaListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, StanzaListener>(); 054 protected ConcurrentHashMap<NodeConfigListener, StanzaListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, StanzaListener>(); 055 056 /** 057 * Construct a node associated to the supplied connection with the specified 058 * node id. 059 * 060 * @param connection The connection the node is associated with 061 * @param nodeName The node id 062 */ 063 Node(PubSubManager pubSubManager, String nodeId) 064 { 065 this.pubSubManager = pubSubManager; 066 id = nodeId; 067 } 068 069 /** 070 * Get the NodeId. 071 * 072 * @return the node id 073 */ 074 public String getId() 075 { 076 return id; 077 } 078 /** 079 * Returns a configuration form, from which you can create an answer form to be submitted 080 * via the {@link #sendConfigurationForm(Form)}. 081 * 082 * @return the configuration form 083 * @throws XMPPErrorException 084 * @throws NoResponseException 085 * @throws NotConnectedException 086 * @throws InterruptedException 087 */ 088 public ConfigureForm getNodeConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 089 { 090 PubSub pubSub = createPubsubPacket(Type.get, new NodeExtension( 091 PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER); 092 Stanza reply = sendPubsubPacket(pubSub); 093 return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER); 094 } 095 096 /** 097 * Update the configuration with the contents of the new {@link Form}. 098 * 099 * @param submitForm 100 * @throws XMPPErrorException 101 * @throws NoResponseException 102 * @throws NotConnectedException 103 * @throws InterruptedException 104 */ 105 public void sendConfigurationForm(Form submitForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 106 { 107 PubSub packet = createPubsubPacket(Type.set, new FormNode(FormNodeType.CONFIGURE_OWNER, 108 getId(), submitForm), PubSubNamespace.OWNER); 109 pubSubManager.getConnection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); 110 } 111 112 /** 113 * Discover node information in standard {@link DiscoverInfo} format. 114 * 115 * @return The discovery information about the node. 116 * @throws XMPPErrorException 117 * @throws NoResponseException if there was no response from the server. 118 * @throws NotConnectedException 119 * @throws InterruptedException 120 */ 121 public DiscoverInfo discoverInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 122 { 123 DiscoverInfo info = new DiscoverInfo(); 124 info.setTo(pubSubManager.getServiceJid()); 125 info.setNode(getId()); 126 return pubSubManager.getConnection().createStanzaCollectorAndSend(info).nextResultOrThrow(); 127 } 128 129 /** 130 * Get the subscriptions currently associated with this node. 131 * 132 * @return List of {@link Subscription} 133 * @throws XMPPErrorException 134 * @throws NoResponseException 135 * @throws NotConnectedException 136 * @throws InterruptedException 137 * 138 */ 139 public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 140 { 141 return getSubscriptions(null, null); 142 } 143 144 /** 145 * Get the subscriptions currently associated with this node. 146 * <p> 147 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 148 * {@code returnedExtensions} will be filled with the stanza(/packet) extensions found in the answer. 149 * </p> 150 * 151 * @param additionalExtensions 152 * @param returnedExtensions a collection that will be filled with the returned packet 153 * extensions 154 * @return List of {@link Subscription} 155 * @throws NoResponseException 156 * @throws XMPPErrorException 157 * @throws NotConnectedException 158 * @throws InterruptedException 159 */ 160 public List<Subscription> getSubscriptions(List<ExtensionElement> additionalExtensions, Collection<ExtensionElement> returnedExtensions) 161 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 162 return getSubscriptions(additionalExtensions, returnedExtensions, null); 163 } 164 165 /** 166 * Get the subscriptions currently associated with this node as owner. 167 * 168 * @return List of {@link Subscription} 169 * @throws XMPPErrorException 170 * @throws NoResponseException 171 * @throws NotConnectedException 172 * @throws InterruptedException 173 * @see #getSubscriptionsAsOwner(List, Collection) 174 * @since 4.1 175 */ 176 public List<Subscription> getSubscriptionsAsOwner() throws NoResponseException, XMPPErrorException, 177 NotConnectedException, InterruptedException { 178 return getSubscriptionsAsOwner(null, null); 179 } 180 181 /** 182 * Get the subscriptions currently associated with this node as owner. 183 * <p> 184 * Unlike {@link #getSubscriptions(List, Collection)}, which only retrieves the subscriptions of the current entity 185 * ("user"), this method returns a list of <b>all</b> subscriptions. This requires the entity to have the sufficient 186 * privileges to manage subscriptions. 187 * </p> 188 * <p> 189 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 190 * {@code returnedExtensions} will be filled with the stanza(/packet) extensions found in the answer. 191 * </p> 192 * 193 * @param additionalExtensions 194 * @param returnedExtensions a collection that will be filled with the returned stanza(/packet) extensions 195 * @return List of {@link Subscription} 196 * @throws NoResponseException 197 * @throws XMPPErrorException 198 * @throws NotConnectedException 199 * @throws InterruptedException 200 * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-subscriptions-retrieve">XEP-60 § 8.8.1 - 201 * Retrieve Subscriptions List</a> 202 * @since 4.1 203 */ 204 public List<Subscription> getSubscriptionsAsOwner(List<ExtensionElement> additionalExtensions, 205 Collection<ExtensionElement> returnedExtensions) throws NoResponseException, XMPPErrorException, 206 NotConnectedException, InterruptedException { 207 return getSubscriptions(additionalExtensions, returnedExtensions, PubSubNamespace.OWNER); 208 } 209 210 private List<Subscription> getSubscriptions(List<ExtensionElement> additionalExtensions, 211 Collection<ExtensionElement> returnedExtensions, PubSubNamespace pubSubNamespace) 212 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 213 PubSub pubSub = createPubsubPacket(Type.get, new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()), pubSubNamespace); 214 if (additionalExtensions != null) { 215 for (ExtensionElement pe : additionalExtensions) { 216 pubSub.addExtension(pe); 217 } 218 } 219 PubSub reply = sendPubsubPacket(pubSub); 220 if (returnedExtensions != null) { 221 returnedExtensions.addAll(reply.getExtensions()); 222 } 223 SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtension(PubSubElementType.SUBSCRIPTIONS); 224 return subElem.getSubscriptions(); 225 } 226 227 /** 228 * Get the affiliations of this node. 229 * 230 * @return List of {@link Affiliation} 231 * @throws NoResponseException 232 * @throws XMPPErrorException 233 * @throws NotConnectedException 234 * @throws InterruptedException 235 */ 236 public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException, 237 NotConnectedException, InterruptedException { 238 return getAffiliations(null, null); 239 } 240 241 /** 242 * Get the affiliations of this node. 243 * <p> 244 * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension. 245 * {@code returnedExtensions} will be filled with the stanza(/packet) extensions found in the answer. 246 * </p> 247 * 248 * @param additionalExtensions additional {@code PacketExtensions} add to the request 249 * @param returnedExtensions a collection that will be filled with the returned packet 250 * extensions 251 * @return List of {@link Affiliation} 252 * @throws NoResponseException 253 * @throws XMPPErrorException 254 * @throws NotConnectedException 255 * @throws InterruptedException 256 */ 257 public List<Affiliation> getAffiliations(List<ExtensionElement> additionalExtensions, Collection<ExtensionElement> returnedExtensions) 258 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 259 260 return getAffiliations(PubSubNamespace.BASIC, additionalExtensions, returnedExtensions); 261 } 262 263 /** 264 * Retrieve the affiliation list for this node as owner. 265 * 266 * @return list of entities whose affiliation is not 'none'. 267 * @throws NoResponseException 268 * @throws XMPPErrorException 269 * @throws NotConnectedException 270 * @throws InterruptedException 271 * @see #getAffiliations(List, Collection) 272 * @since 4.2 273 */ 274 public List<Affiliation> getAffiliationsAsOwner() 275 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 276 277 return getAffiliationsAsOwner(null, null); 278 } 279 280 /** 281 * Retrieve the affiliation list for this node as owner. 282 * <p> 283 * Note that this is an <b>optional</b> PubSub feature ('pubusb#modify-affiliations'). 284 * </p> 285 * 286 * @param additionalExtensions optional additional extension elements add to the request. 287 * @param returnedExtensions an optional collection that will be filled with the returned 288 * extension elements. 289 * @return list of entities whose affiliation is not 'none'. 290 * @throws NoResponseException 291 * @throws XMPPErrorException 292 * @throws NotConnectedException 293 * @throws InterruptedException 294 * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-affiliations-retrieve">XEP-60 § 8.9.1 Retrieve Affiliations List</a> 295 * @since 4.2 296 */ 297 public List<Affiliation> getAffiliationsAsOwner(List<ExtensionElement> additionalExtensions, Collection<ExtensionElement> returnedExtensions) 298 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 299 300 return getAffiliations(PubSubNamespace.OWNER, additionalExtensions, returnedExtensions); 301 } 302 303 private List<Affiliation> getAffiliations(PubSubNamespace namespace, List<ExtensionElement> additionalExtensions, 304 Collection<ExtensionElement> returnedExtensions) throws NoResponseException, XMPPErrorException, 305 NotConnectedException, InterruptedException { 306 307 PubSub pubSub = createPubsubPacket(Type.get, new NodeExtension(PubSubElementType.AFFILIATIONS, getId()), namespace); 308 if (additionalExtensions != null) { 309 for (ExtensionElement pe : additionalExtensions) { 310 pubSub.addExtension(pe); 311 } 312 } 313 PubSub reply = sendPubsubPacket(pubSub); 314 if (returnedExtensions != null) { 315 returnedExtensions.addAll(reply.getExtensions()); 316 } 317 AffiliationsExtension affilElem = (AffiliationsExtension) reply.getExtension(PubSubElementType.AFFILIATIONS); 318 return affilElem.getAffiliations(); 319 } 320 321 /** 322 * Modify the affiliations for this PubSub node as owner. The {@link Affiliation}s given must be created with the 323 * {@link Affiliation#Affiliation(org.jxmpp.jid.BareJid, Affiliation.Type)} constructor. 324 * <p> 325 * Note that this is an <b>optional</b> PubSub feature ('pubusb#modify-affiliations'). 326 * </p> 327 * 328 * @param affiliations 329 * @return <code>null</code> or a PubSub stanza with additional information on success. 330 * @throws NoResponseException 331 * @throws XMPPErrorException 332 * @throws NotConnectedException 333 * @throws InterruptedException 334 * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-affiliations-modify">XEP-60 § 8.9.2 Modify Affiliation</a> 335 * @since 4.2 336 */ 337 public PubSub modifyAffiliationAsOwner(List<Affiliation> affiliations) throws NoResponseException, 338 XMPPErrorException, NotConnectedException, InterruptedException { 339 for (Affiliation affiliation : affiliations) { 340 if (affiliation.getPubSubNamespace() != PubSubNamespace.OWNER) { 341 throw new IllegalArgumentException("Must use Affiliation(BareJid, Type) affiliations"); 342 } 343 } 344 345 PubSub pubSub = createPubsubPacket(Type.set, new AffiliationsExtension(affiliations, getId()), 346 PubSubNamespace.OWNER); 347 return sendPubsubPacket(pubSub); 348 } 349 350 /** 351 * The user subscribes to the node using the supplied jid. The 352 * bare jid portion of this one must match the jid for the connection. 353 * 354 * Please note that the {@link Subscription.State} should be checked 355 * on return since more actions may be required by the caller. 356 * {@link Subscription.State#pending} - The owner must approve the subscription 357 * request before messages will be received. 358 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 359 * the caller must configure the subscription before messages will be received. If it is false 360 * the caller can configure it but is not required to do so. 361 * @param jid The jid to subscribe as. 362 * @return The subscription 363 * @throws XMPPErrorException 364 * @throws NoResponseException 365 * @throws NotConnectedException 366 * @throws InterruptedException 367 */ 368 public Subscription subscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 369 { 370 PubSub pubSub = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); 371 PubSub reply = sendPubsubPacket(pubSub); 372 return reply.getExtension(PubSubElementType.SUBSCRIPTION); 373 } 374 375 /** 376 * The user subscribes to the node using the supplied jid and subscription 377 * options. The bare jid portion of this one must match the jid for the 378 * connection. 379 * 380 * Please note that the {@link Subscription.State} should be checked 381 * on return since more actions may be required by the caller. 382 * {@link Subscription.State#pending} - The owner must approve the subscription 383 * request before messages will be received. 384 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 385 * the caller must configure the subscription before messages will be received. If it is false 386 * the caller can configure it but is not required to do so. 387 * @param jid The jid to subscribe as. 388 * @return The subscription 389 * @throws XMPPErrorException 390 * @throws NoResponseException 391 * @throws NotConnectedException 392 * @throws InterruptedException 393 */ 394 public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 395 { 396 PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); 397 request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); 398 PubSub reply = sendPubsubPacket(request); 399 return reply.getExtension(PubSubElementType.SUBSCRIPTION); 400 } 401 402 /** 403 * Remove the subscription related to the specified JID. This will only 404 * work if there is only 1 subscription. If there are multiple subscriptions, 405 * use {@link #unsubscribe(String, String)}. 406 * 407 * @param jid The JID used to subscribe to the node 408 * @throws XMPPErrorException 409 * @throws NoResponseException 410 * @throws NotConnectedException 411 * @throws InterruptedException 412 * 413 */ 414 public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 415 { 416 unsubscribe(jid, null); 417 } 418 419 /** 420 * Remove the specific subscription related to the specified JID. 421 * 422 * @param jid The JID used to subscribe to the node 423 * @param subscriptionId The id of the subscription being removed 424 * @throws XMPPErrorException 425 * @throws NoResponseException 426 * @throws NotConnectedException 427 * @throws InterruptedException 428 */ 429 public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 430 { 431 sendPubsubPacket(createPubsubPacket(Type.set, new UnsubscribeExtension(jid, getId(), subscriptionId))); 432 } 433 434 /** 435 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted 436 * via the {@link #sendConfigurationForm(Form)}. 437 * 438 * @return A subscription options form 439 * @throws XMPPErrorException 440 * @throws NoResponseException 441 * @throws NotConnectedException 442 * @throws InterruptedException 443 */ 444 public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 445 { 446 return getSubscriptionOptions(jid, null); 447 } 448 449 450 /** 451 * Get the options for configuring the specified subscription. 452 * 453 * @param jid JID the subscription is registered under 454 * @param subscriptionId The subscription id 455 * 456 * @return The subscription option form 457 * @throws XMPPErrorException 458 * @throws NoResponseException 459 * @throws NotConnectedException 460 * @throws InterruptedException 461 * 462 */ 463 public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 464 { 465 PubSub packet = sendPubsubPacket(createPubsubPacket(Type.get, new OptionsExtension(jid, getId(), subscriptionId))); 466 FormNode ext = packet.getExtension(PubSubElementType.OPTIONS); 467 return new SubscribeForm(ext.getForm()); 468 } 469 470 /** 471 * Register a listener for item publication events. This 472 * listener will get called whenever an item is published to 473 * this node. 474 * 475 * @param listener The handler for the event 476 */ 477 @SuppressWarnings("unchecked") 478 public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 479 { 480 StanzaListener conListener = new ItemEventTranslator(listener); 481 itemEventToListenerMap.put(listener, conListener); 482 pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); 483 } 484 485 /** 486 * Unregister a listener for publication events. 487 * 488 * @param listener The handler to unregister 489 */ 490 public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 491 { 492 StanzaListener conListener = itemEventToListenerMap.remove(listener); 493 494 if (conListener != null) 495 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 496 } 497 498 /** 499 * Register a listener for configuration events. This listener 500 * will get called whenever the node's configuration changes. 501 * 502 * @param listener The handler for the event 503 */ 504 public void addConfigurationListener(NodeConfigListener listener) 505 { 506 StanzaListener conListener = new NodeConfigTranslator(listener); 507 configEventToListenerMap.put(listener, conListener); 508 pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); 509 } 510 511 /** 512 * Unregister a listener for configuration events. 513 * 514 * @param listener The handler to unregister 515 */ 516 public void removeConfigurationListener(NodeConfigListener listener) 517 { 518 StanzaListener conListener = configEventToListenerMap .remove(listener); 519 520 if (conListener != null) 521 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 522 } 523 524 /** 525 * Register an listener for item delete events. This listener 526 * gets called whenever an item is deleted from the node. 527 * 528 * @param listener The handler for the event 529 */ 530 public void addItemDeleteListener(ItemDeleteListener listener) 531 { 532 StanzaListener delListener = new ItemDeleteTranslator(listener); 533 itemDeleteToListenerMap.put(listener, delListener); 534 EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract"); 535 EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString()); 536 537 pubSubManager.getConnection().addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge)); 538 } 539 540 /** 541 * Unregister a listener for item delete events. 542 * 543 * @param listener The handler to unregister 544 */ 545 public void removeItemDeleteListener(ItemDeleteListener listener) 546 { 547 StanzaListener conListener = itemDeleteToListenerMap .remove(listener); 548 549 if (conListener != null) 550 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 551 } 552 553 @Override 554 public String toString() 555 { 556 return super.toString() + " " + getClass().getName() + " id: " + id; 557 } 558 559 protected PubSub createPubsubPacket(Type type, ExtensionElement ext) 560 { 561 return createPubsubPacket(type, ext, null); 562 } 563 564 protected PubSub createPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 565 { 566 return PubSub.createPubsubPacket(pubSubManager.getServiceJid(), type, ext, ns); 567 } 568 569 protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 570 { 571 return pubSubManager.sendPubsubPacket(packet); 572 } 573 574 575 private static List<String> getSubscriptionIds(Stanza packet) 576 { 577 HeadersExtension headers = (HeadersExtension) packet.getExtension("headers", "http://jabber.org/protocol/shim"); 578 List<String> values = null; 579 580 if (headers != null) 581 { 582 values = new ArrayList<String>(headers.getHeaders().size()); 583 584 for (Header header : headers.getHeaders()) 585 { 586 values.add(header.getValue()); 587 } 588 } 589 return values; 590 } 591 592 /** 593 * This class translates low level item publication events into api level objects for 594 * user consumption. 595 * 596 * @author Robin Collier 597 */ 598 public class ItemEventTranslator implements StanzaListener 599 { 600 @SuppressWarnings("rawtypes") 601 private ItemEventListener listener; 602 603 public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener) 604 { 605 listener = eventListener; 606 } 607 608 @Override 609 @SuppressWarnings({ "rawtypes", "unchecked" }) 610 public void processStanza(Stanza packet) 611 { 612 EventElement event = (EventElement) packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 613 ItemsExtension itemsElem = (ItemsExtension) event.getEvent(); 614 ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), itemsElem.getItems(), getSubscriptionIds(packet), DelayInformationManager.getDelayTimestamp(packet)); 615 listener.handlePublishedItems(eventItems); 616 } 617 } 618 619 /** 620 * This class translates low level item deletion events into api level objects for 621 * user consumption. 622 * 623 * @author Robin Collier 624 */ 625 public class ItemDeleteTranslator implements StanzaListener 626 { 627 private ItemDeleteListener listener; 628 629 public ItemDeleteTranslator(ItemDeleteListener eventListener) 630 { 631 listener = eventListener; 632 } 633 634 @Override 635 public void processStanza(Stanza packet) 636 { 637// CHECKSTYLE:OFF 638 EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 639 640 List<ExtensionElement> extList = event.getExtensions(); 641 642 if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName())) 643 { 644 listener.handlePurge(); 645 } 646 else 647 { 648 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 649 @SuppressWarnings("unchecked") 650 Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems(); 651 List<String> items = new ArrayList<String>(pubItems.size()); 652 653 for (RetractItem item : pubItems) 654 { 655 items.add(item.getId()); 656 } 657 658 ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet)); 659 listener.handleDeletedItems(eventItems); 660 } 661// CHECKSTYLE:ON 662 } 663 } 664 665 /** 666 * This class translates low level node configuration events into api level objects for 667 * user consumption. 668 * 669 * @author Robin Collier 670 */ 671 public static class NodeConfigTranslator implements StanzaListener 672 { 673 private NodeConfigListener listener; 674 675 public NodeConfigTranslator(NodeConfigListener eventListener) 676 { 677 listener = eventListener; 678 } 679 680 @Override 681 public void processStanza(Stanza packet) 682 { 683 EventElement event = (EventElement) packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 684 ConfigurationEvent config = (ConfigurationEvent) event.getEvent(); 685 686 listener.handleNodeConfiguration(config); 687 } 688 } 689 690 /** 691 * Filter for {@link StanzaListener} to filter out events not specific to the 692 * event type expected for this node. 693 * 694 * @author Robin Collier 695 */ 696 class EventContentFilter extends FlexibleStanzaTypeFilter<Message> 697 { 698 private final String firstElement; 699 private final String secondElement; 700 private final boolean allowEmpty; 701 702 EventContentFilter(String elementName) 703 { 704 this(elementName, null); 705 } 706 707 EventContentFilter(String firstLevelEelement, String secondLevelElement) 708 { 709 firstElement = firstLevelEelement; 710 secondElement = secondLevelElement; 711 allowEmpty = firstElement.equals(EventElementType.items.toString()) 712 && "item".equals(secondLevelElement); 713 } 714 715 @Override 716 public boolean acceptSpecific(Message message) { 717 EventElement event = EventElement.from(message); 718 719 if (event == null) 720 return false; 721 722 NodeExtension embedEvent = event.getEvent(); 723 724 if (embedEvent == null) 725 return false; 726 727 if (embedEvent.getElementName().equals(firstElement)) 728 { 729 if (!embedEvent.getNode().equals(getId())) 730 return false; 731 732 if (secondElement == null) 733 return true; 734 735 if (embedEvent instanceof EmbeddedPacketExtension) 736 { 737 List<ExtensionElement> secondLevelList = ((EmbeddedPacketExtension) embedEvent).getExtensions(); 738 739 // XEP-0060 allows no elements on second level for notifications. See schema or 740 // for example § 4.3: 741 // "although event notifications MUST include an empty <items/> element;" 742 if (allowEmpty && secondLevelList.isEmpty()) { 743 return true; 744 } 745 746 if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement)) 747 return true; 748 } 749 } 750 return false; 751 } 752 } 753}