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