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