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}