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