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