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