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