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<>(); 053 protected ConcurrentHashMap<ItemDeleteListener, StanzaListener> itemDeleteToListenerMap = new ConcurrentHashMap<>(); 054 protected ConcurrentHashMap<NodeConfigListener, StanzaListener> configEventToListenerMap = new ConcurrentHashMap<>(); 055 056 /** 057 * Construct a node associated to the supplied connection with the specified 058 * node id. 059 * 060 * @param pubSubManager The PubSubManager for the connection the node is associated with 061 * @param nodeId 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 = 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 ('pubsub#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 = 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 ('pubsub#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 * 388 * @param jid The jid to subscribe as. 389 * @param subForm 390 * 391 * @return The subscription 392 * @throws XMPPErrorException 393 * @throws NoResponseException 394 * @throws NotConnectedException 395 * @throws InterruptedException 396 */ 397 public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 398 { 399 PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId())); 400 request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm)); 401 PubSub reply = sendPubsubPacket(request); 402 return reply.getExtension(PubSubElementType.SUBSCRIPTION); 403 } 404 405 /** 406 * Remove the subscription related to the specified JID. This will only 407 * work if there is only 1 subscription. If there are multiple subscriptions, 408 * use {@link #unsubscribe(String, String)}. 409 * 410 * @param jid The JID used to subscribe to the node 411 * @throws XMPPErrorException 412 * @throws NoResponseException 413 * @throws NotConnectedException 414 * @throws InterruptedException 415 * 416 */ 417 public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 418 { 419 unsubscribe(jid, null); 420 } 421 422 /** 423 * Remove the specific subscription related to the specified JID. 424 * 425 * @param jid The JID used to subscribe to the node 426 * @param subscriptionId The id of the subscription being removed 427 * @throws XMPPErrorException 428 * @throws NoResponseException 429 * @throws NotConnectedException 430 * @throws InterruptedException 431 */ 432 public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 433 { 434 sendPubsubPacket(createPubsubPacket(Type.set, new UnsubscribeExtension(jid, getId(), subscriptionId))); 435 } 436 437 /** 438 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted 439 * via the {@link #sendConfigurationForm(Form)}. 440 * 441 * @param jid 442 * 443 * @return A subscription options form 444 * @throws XMPPErrorException 445 * @throws NoResponseException 446 * @throws NotConnectedException 447 * @throws InterruptedException 448 */ 449 public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 450 { 451 return getSubscriptionOptions(jid, null); 452 } 453 454 455 /** 456 * Get the options for configuring the specified subscription. 457 * 458 * @param jid JID the subscription is registered under 459 * @param subscriptionId The subscription id 460 * 461 * @return The subscription option form 462 * @throws XMPPErrorException 463 * @throws NoResponseException 464 * @throws NotConnectedException 465 * @throws InterruptedException 466 * 467 */ 468 public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 469 { 470 PubSub packet = sendPubsubPacket(createPubsubPacket(Type.get, new OptionsExtension(jid, getId(), subscriptionId))); 471 FormNode ext = packet.getExtension(PubSubElementType.OPTIONS); 472 return new SubscribeForm(ext.getForm()); 473 } 474 475 /** 476 * Register a listener for item publication events. This 477 * listener will get called whenever an item is published to 478 * this node. 479 * 480 * @param listener The handler for the event 481 */ 482 @SuppressWarnings("unchecked") 483 public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 484 { 485 StanzaListener conListener = new ItemEventTranslator(listener); 486 itemEventToListenerMap.put(listener, conListener); 487 pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item")); 488 } 489 490 /** 491 * Unregister a listener for publication events. 492 * 493 * @param listener The handler to unregister 494 */ 495 public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) 496 { 497 StanzaListener conListener = itemEventToListenerMap.remove(listener); 498 499 if (conListener != null) 500 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 501 } 502 503 /** 504 * Register a listener for configuration events. This listener 505 * will get called whenever the node's configuration changes. 506 * 507 * @param listener The handler for the event 508 */ 509 public void addConfigurationListener(NodeConfigListener listener) 510 { 511 StanzaListener conListener = new NodeConfigTranslator(listener); 512 configEventToListenerMap.put(listener, conListener); 513 pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString())); 514 } 515 516 /** 517 * Unregister a listener for configuration events. 518 * 519 * @param listener The handler to unregister 520 */ 521 public void removeConfigurationListener(NodeConfigListener listener) 522 { 523 StanzaListener conListener = configEventToListenerMap .remove(listener); 524 525 if (conListener != null) 526 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 527 } 528 529 /** 530 * Register an listener for item delete events. This listener 531 * gets called whenever an item is deleted from the node. 532 * 533 * @param listener The handler for the event 534 */ 535 public void addItemDeleteListener(ItemDeleteListener listener) 536 { 537 StanzaListener delListener = new ItemDeleteTranslator(listener); 538 itemDeleteToListenerMap.put(listener, delListener); 539 EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract"); 540 EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString()); 541 542 pubSubManager.getConnection().addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge)); 543 } 544 545 /** 546 * Unregister a listener for item delete events. 547 * 548 * @param listener The handler to unregister 549 */ 550 public void removeItemDeleteListener(ItemDeleteListener listener) 551 { 552 StanzaListener conListener = itemDeleteToListenerMap .remove(listener); 553 554 if (conListener != null) 555 pubSubManager.getConnection().removeSyncStanzaListener(conListener); 556 } 557 558 @Override 559 public String toString() 560 { 561 return super.toString() + " " + getClass().getName() + " id: " + id; 562 } 563 564 protected PubSub createPubsubPacket(Type type, ExtensionElement ext) 565 { 566 return createPubsubPacket(type, ext, null); 567 } 568 569 protected PubSub createPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns) 570 { 571 return PubSub.createPubsubPacket(pubSubManager.getServiceJid(), type, ext, ns); 572 } 573 574 protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 575 { 576 return pubSubManager.sendPubsubPacket(packet); 577 } 578 579 580 private static List<String> getSubscriptionIds(Stanza packet) 581 { 582 HeadersExtension headers = packet.getExtension("headers", "http://jabber.org/protocol/shim"); 583 List<String> values = null; 584 585 if (headers != null) 586 { 587 values = new ArrayList<>(headers.getHeaders().size()); 588 589 for (Header header : headers.getHeaders()) 590 { 591 values.add(header.getValue()); 592 } 593 } 594 return values; 595 } 596 597 /** 598 * This class translates low level item publication events into api level objects for 599 * user consumption. 600 * 601 * @author Robin Collier 602 */ 603 public class ItemEventTranslator implements StanzaListener 604 { 605 @SuppressWarnings("rawtypes") 606 private final ItemEventListener listener; 607 608 public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener) 609 { 610 listener = eventListener; 611 } 612 613 @Override 614 @SuppressWarnings({ "rawtypes", "unchecked" }) 615 public void processStanza(Stanza packet) 616 { 617 EventElement event = packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 618 ItemsExtension itemsElem = (ItemsExtension) event.getEvent(); 619 ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), itemsElem.getItems(), getSubscriptionIds(packet), DelayInformationManager.getDelayTimestamp(packet)); 620 listener.handlePublishedItems(eventItems); 621 } 622 } 623 624 /** 625 * This class translates low level item deletion events into api level objects for 626 * user consumption. 627 * 628 * @author Robin Collier 629 */ 630 public class ItemDeleteTranslator implements StanzaListener 631 { 632 private final ItemDeleteListener listener; 633 634 public ItemDeleteTranslator(ItemDeleteListener eventListener) 635 { 636 listener = eventListener; 637 } 638 639 @Override 640 public void processStanza(Stanza packet) 641 { 642// CHECKSTYLE:OFF 643 EventElement event = packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 644 645 List<ExtensionElement> extList = event.getExtensions(); 646 647 if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName())) 648 { 649 listener.handlePurge(); 650 } 651 else 652 { 653 ItemsExtension itemsElem = (ItemsExtension)event.getEvent(); 654 @SuppressWarnings("unchecked") 655 Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems(); 656 List<String> items = new ArrayList<>(pubItems.size()); 657 658 for (RetractItem item : pubItems) 659 { 660 items.add(item.getId()); 661 } 662 663 ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet)); 664 listener.handleDeletedItems(eventItems); 665 } 666// CHECKSTYLE:ON 667 } 668 } 669 670 /** 671 * This class translates low level node configuration events into api level objects for 672 * user consumption. 673 * 674 * @author Robin Collier 675 */ 676 public static class NodeConfigTranslator implements StanzaListener 677 { 678 private final NodeConfigListener listener; 679 680 public NodeConfigTranslator(NodeConfigListener eventListener) 681 { 682 listener = eventListener; 683 } 684 685 @Override 686 public void processStanza(Stanza packet) 687 { 688 EventElement event = packet.getExtension("event", PubSubNamespace.EVENT.getXmlns()); 689 ConfigurationEvent config = (ConfigurationEvent) event.getEvent(); 690 691 listener.handleNodeConfiguration(config); 692 } 693 } 694 695 /** 696 * Filter for {@link StanzaListener} to filter out events not specific to the 697 * event type expected for this node. 698 * 699 * @author Robin Collier 700 */ 701 class EventContentFilter extends FlexibleStanzaTypeFilter<Message> 702 { 703 private final String firstElement; 704 private final String secondElement; 705 private final boolean allowEmpty; 706 707 EventContentFilter(String elementName) 708 { 709 this(elementName, null); 710 } 711 712 EventContentFilter(String firstLevelElement, String secondLevelElement) 713 { 714 firstElement = firstLevelElement; 715 secondElement = secondLevelElement; 716 allowEmpty = firstElement.equals(EventElementType.items.toString()) 717 && "item".equals(secondLevelElement); 718 } 719 720 @Override 721 public boolean acceptSpecific(Message message) { 722 EventElement event = EventElement.from(message); 723 724 if (event == null) 725 return false; 726 727 NodeExtension embedEvent = event.getEvent(); 728 729 if (embedEvent == null) 730 return false; 731 732 if (embedEvent.getElementName().equals(firstElement)) 733 { 734 if (!embedEvent.getNode().equals(getId())) 735 return false; 736 737 if (secondElement == null) 738 return true; 739 740 if (embedEvent instanceof EmbeddedPacketExtension) 741 { 742 List<ExtensionElement> secondLevelList = ((EmbeddedPacketExtension) embedEvent).getExtensions(); 743 744 // XEP-0060 allows no elements on second level for notifications. See schema or 745 // for example § 4.3: 746 // "although event notifications MUST include an empty <items/> element;" 747 if (allowEmpty && secondLevelList.isEmpty()) { 748 return true; 749 } 750 751 if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement)) 752 return true; 753 } 754 } 755 return false; 756 } 757 } 758}