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<ItemEventListener<Item>, StanzaListener>();
053    protected ConcurrentHashMap<ItemDeleteListener, StanzaListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, StanzaListener>();
054    protected ConcurrentHashMap<NodeConfigListener, StanzaListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, StanzaListener>();
055
056    /**
057     * Construct a node associated to the supplied connection with the specified 
058     * node id.
059     * 
060     * @param connection The connection the node is associated with
061     * @param nodeName 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 = (SubscriptionsExtension) 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 ('pubusb#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 = (AffiliationsExtension) 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 ('pubusb#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     * @param jid The jid to subscribe as.
388     * @return The subscription
389     * @throws XMPPErrorException 
390     * @throws NoResponseException 
391     * @throws NotConnectedException 
392     * @throws InterruptedException 
393     */
394    public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
395    {
396        PubSub request = createPubsubPacket(Type.set, new SubscribeExtension(jid, getId()));
397        request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
398        PubSub reply = sendPubsubPacket(request);
399        return reply.getExtension(PubSubElementType.SUBSCRIPTION);
400    }
401
402    /**
403     * Remove the subscription related to the specified JID.  This will only 
404     * work if there is only 1 subscription.  If there are multiple subscriptions,
405     * use {@link #unsubscribe(String, String)}.
406     * 
407     * @param jid The JID used to subscribe to the node
408     * @throws XMPPErrorException 
409     * @throws NoResponseException 
410     * @throws NotConnectedException 
411     * @throws InterruptedException 
412     * 
413     */
414    public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
415    {
416        unsubscribe(jid, null);
417    }
418
419    /**
420     * Remove the specific subscription related to the specified JID.
421     * 
422     * @param jid The JID used to subscribe to the node
423     * @param subscriptionId The id of the subscription being removed
424     * @throws XMPPErrorException 
425     * @throws NoResponseException 
426     * @throws NotConnectedException 
427     * @throws InterruptedException 
428     */
429    public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
430    {
431        sendPubsubPacket(createPubsubPacket(Type.set, new UnsubscribeExtension(jid, getId(), subscriptionId)));
432    }
433
434    /**
435     * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
436     * via the {@link #sendConfigurationForm(Form)}.
437     * 
438     * @return A subscription options form
439     * @throws XMPPErrorException 
440     * @throws NoResponseException 
441     * @throws NotConnectedException 
442     * @throws InterruptedException 
443     */
444    public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
445    {
446        return getSubscriptionOptions(jid, null);
447    }
448
449
450    /**
451     * Get the options for configuring the specified subscription.
452     * 
453     * @param jid JID the subscription is registered under
454     * @param subscriptionId The subscription id
455     * 
456     * @return The subscription option form
457     * @throws XMPPErrorException 
458     * @throws NoResponseException 
459     * @throws NotConnectedException 
460     * @throws InterruptedException 
461     * 
462     */
463    public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
464    {
465        PubSub packet = sendPubsubPacket(createPubsubPacket(Type.get, new OptionsExtension(jid, getId(), subscriptionId)));
466        FormNode ext = packet.getExtension(PubSubElementType.OPTIONS);
467        return new SubscribeForm(ext.getForm());
468    }
469
470    /**
471     * Register a listener for item publication events.  This 
472     * listener will get called whenever an item is published to 
473     * this node.
474     * 
475     * @param listener The handler for the event
476     */
477    @SuppressWarnings("unchecked")
478    public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener)
479    {
480        StanzaListener conListener = new ItemEventTranslator(listener); 
481        itemEventToListenerMap.put(listener, conListener);
482        pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));
483    }
484
485    /**
486     * Unregister a listener for publication events.
487     * 
488     * @param listener The handler to unregister
489     */
490    public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener)
491    {
492        StanzaListener conListener = itemEventToListenerMap.remove(listener);
493
494        if (conListener != null)
495            pubSubManager.getConnection().removeSyncStanzaListener(conListener);
496    }
497
498    /**
499     * Register a listener for configuration events.  This listener
500     * will get called whenever the node's configuration changes.
501     * 
502     * @param listener The handler for the event
503     */
504    public void addConfigurationListener(NodeConfigListener listener)
505    {
506        StanzaListener conListener = new NodeConfigTranslator(listener); 
507        configEventToListenerMap.put(listener, conListener);
508        pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));
509    }
510
511    /**
512     * Unregister a listener for configuration events.
513     * 
514     * @param listener The handler to unregister
515     */
516    public void removeConfigurationListener(NodeConfigListener listener)
517    {
518        StanzaListener conListener = configEventToListenerMap .remove(listener);
519
520        if (conListener != null)
521            pubSubManager.getConnection().removeSyncStanzaListener(conListener);
522    }
523
524    /**
525     * Register an listener for item delete events.  This listener
526     * gets called whenever an item is deleted from the node.
527     * 
528     * @param listener The handler for the event
529     */
530    public void addItemDeleteListener(ItemDeleteListener listener)
531    {
532        StanzaListener delListener = new ItemDeleteTranslator(listener); 
533        itemDeleteToListenerMap.put(listener, delListener);
534        EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");
535        EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());
536
537        pubSubManager.getConnection().addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge));
538    }
539
540    /**
541     * Unregister a listener for item delete events.
542     * 
543     * @param listener The handler to unregister
544     */
545    public void removeItemDeleteListener(ItemDeleteListener listener)
546    {
547        StanzaListener conListener = itemDeleteToListenerMap .remove(listener);
548
549        if (conListener != null)
550            pubSubManager.getConnection().removeSyncStanzaListener(conListener);
551    }
552
553    @Override
554    public String toString()
555    {
556        return super.toString() + " " + getClass().getName() + " id: " + id;
557    }
558
559    protected PubSub createPubsubPacket(Type type, ExtensionElement ext)
560    {
561        return createPubsubPacket(type, ext, null);
562    }
563
564    protected PubSub createPubsubPacket(Type type, ExtensionElement ext, PubSubNamespace ns)
565    {
566        return PubSub.createPubsubPacket(pubSubManager.getServiceJid(), type, ext, ns);
567    }
568
569    protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
570    {
571        return pubSubManager.sendPubsubPacket(packet);
572    }
573
574
575    private static List<String> getSubscriptionIds(Stanza packet)
576    {
577        HeadersExtension headers = (HeadersExtension) packet.getExtension("headers", "http://jabber.org/protocol/shim");
578        List<String> values = null;
579
580        if (headers != null)
581        {
582            values = new ArrayList<String>(headers.getHeaders().size());
583
584            for (Header header : headers.getHeaders())
585            {
586                values.add(header.getValue());
587            }
588        }
589        return values;
590    }
591
592    /**
593     * This class translates low level item publication events into api level objects for 
594     * user consumption.
595     * 
596     * @author Robin Collier
597     */
598    public class ItemEventTranslator implements StanzaListener
599    {
600        @SuppressWarnings("rawtypes")
601        private ItemEventListener listener;
602
603        public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener)
604        {
605            listener = eventListener;
606        }
607
608        @Override
609        @SuppressWarnings({ "rawtypes", "unchecked" })
610        public void processStanza(Stanza packet)
611        {
612            EventElement event = (EventElement) packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
613            ItemsExtension itemsElem = (ItemsExtension) event.getEvent();
614            ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), itemsElem.getItems(), getSubscriptionIds(packet), DelayInformationManager.getDelayTimestamp(packet));
615            listener.handlePublishedItems(eventItems);
616        }
617    }
618
619    /**
620     * This class translates low level item deletion events into api level objects for 
621     * user consumption.
622     * 
623     * @author Robin Collier
624     */
625    public class ItemDeleteTranslator implements StanzaListener
626    {
627        private ItemDeleteListener listener;
628
629        public ItemDeleteTranslator(ItemDeleteListener eventListener)
630        {
631            listener = eventListener;
632        }
633
634        @Override
635        public void processStanza(Stanza packet)
636        {
637// CHECKSTYLE:OFF
638            EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
639
640            List<ExtensionElement> extList = event.getExtensions();
641
642            if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName()))
643            {
644                listener.handlePurge();
645            }
646            else
647            {
648                ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
649                @SuppressWarnings("unchecked")
650                Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems();
651                List<String> items = new ArrayList<String>(pubItems.size());
652
653                for (RetractItem item : pubItems)
654                {
655                    items.add(item.getId());
656                }
657
658                ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));
659                listener.handleDeletedItems(eventItems);
660            }
661// CHECKSTYLE:ON
662        }
663    }
664
665    /**
666     * This class translates low level node configuration events into api level objects for 
667     * user consumption.
668     * 
669     * @author Robin Collier
670     */
671    public static class NodeConfigTranslator implements StanzaListener
672    {
673        private NodeConfigListener listener;
674
675        public NodeConfigTranslator(NodeConfigListener eventListener)
676        {
677            listener = eventListener;
678        }
679
680        @Override
681        public void processStanza(Stanza packet)
682        {
683            EventElement event = (EventElement) packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
684            ConfigurationEvent config = (ConfigurationEvent) event.getEvent();
685
686            listener.handleNodeConfiguration(config);
687        }
688    }
689
690    /**
691     * Filter for {@link StanzaListener} to filter out events not specific to the 
692     * event type expected for this node.
693     * 
694     * @author Robin Collier
695     */
696    class EventContentFilter extends FlexibleStanzaTypeFilter<Message>
697    {
698        private final String firstElement;
699        private final String secondElement;
700        private final boolean allowEmpty;
701
702        EventContentFilter(String elementName)
703        {
704            this(elementName, null);
705        }
706
707        EventContentFilter(String firstLevelEelement, String secondLevelElement)
708        {
709            firstElement = firstLevelEelement;
710            secondElement = secondLevelElement;
711            allowEmpty = firstElement.equals(EventElementType.items.toString())
712                            && "item".equals(secondLevelElement);
713        }
714
715        @Override
716        public boolean acceptSpecific(Message message) {
717            EventElement event = EventElement.from(message);
718
719            if (event == null)
720                return false;
721
722            NodeExtension embedEvent = event.getEvent();
723
724            if (embedEvent == null)
725                return false;
726
727            if (embedEvent.getElementName().equals(firstElement))
728            {
729                if (!embedEvent.getNode().equals(getId()))
730                    return false;
731
732                if (secondElement == null)
733                    return true;
734
735                if (embedEvent instanceof EmbeddedPacketExtension)
736                {
737                    List<ExtensionElement> secondLevelList = ((EmbeddedPacketExtension) embedEvent).getExtensions();
738
739                    // XEP-0060 allows no elements on second level for notifications. See schema or
740                    // for example § 4.3:
741                    // "although event notifications MUST include an empty <items/> element;"
742                    if (allowEmpty && secondLevelList.isEmpty()) {
743                        return true;
744                    }
745
746                    if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))
747                        return true;
748                }
749            }
750            return false;
751        }
752    }
753}