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