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;
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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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(IQ.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}