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.PacketListener;
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.XMPPConnection;
028import org.jivesoftware.smack.XMPPException.XMPPErrorException;
029import org.jivesoftware.smack.filter.OrFilter;
030import org.jivesoftware.smack.filter.PacketFilter;
031import org.jivesoftware.smack.packet.Message;
032import org.jivesoftware.smack.packet.Packet;
033import org.jivesoftware.smack.packet.PacketExtension;
034import org.jivesoftware.smack.packet.IQ.Type;
035import org.jivesoftware.smackx.delay.packet.DelayInformation;
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 XMPPConnection con;
050        protected String id;
051        protected String to;
052        
053        protected ConcurrentHashMap<ItemEventListener<Item>, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, PacketListener>();
054        protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();
055        protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();
056        
057        /**
058         * Construct a node associated to the supplied connection with the specified 
059         * node id.
060         * 
061         * @param connection The connection the node is associated with
062         * @param nodeName The node id
063         */
064        Node(XMPPConnection connection, String nodeName)
065        {
066                con = connection;
067                id = nodeName;
068        }
069
070        /**
071         * Some XMPP servers may require a specific service to be addressed on the 
072         * server.
073         * 
074         *   For example, OpenFire requires the server to be prefixed by <b>pubsub</b>
075         */
076        void setTo(String toAddress)
077        {
078                to = toAddress;
079        }
080
081        /**
082         * Get the NodeId
083         * 
084         * @return the node id
085         */
086        public String getId() 
087        {
088                return id;
089        }
090        /**
091         * Returns a configuration form, from which you can create an answer form to be submitted
092         * via the {@link #sendConfigurationForm(Form)}.
093         * 
094         * @return the configuration form
095         * @throws XMPPErrorException 
096         * @throws NoResponseException 
097         * @throws NotConnectedException 
098         */
099        public ConfigureForm getNodeConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException
100        {
101        PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension(
102                        PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER);
103                Packet reply = sendPubsubPacket(pubSub);
104                return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER);
105        }
106        
107        /**
108         * Update the configuration with the contents of the new {@link Form}
109         * 
110         * @param submitForm
111         * @throws XMPPErrorException 
112         * @throws NoResponseException 
113         * @throws NotConnectedException 
114         */
115        public void sendConfigurationForm(Form submitForm) throws NoResponseException, XMPPErrorException, NotConnectedException
116        {
117        PubSub packet = createPubsubPacket(Type.SET, new FormNode(FormNodeType.CONFIGURE_OWNER,
118                        getId(), submitForm), PubSubNamespace.OWNER);
119                con.createPacketCollectorAndSend(packet).nextResultOrThrow();
120        }
121        
122        /**
123         * Discover node information in standard {@link DiscoverInfo} format.
124         * 
125         * @return The discovery information about the node.
126         * @throws XMPPErrorException 
127         * @throws NoResponseException if there was no response from the server.
128         * @throws NotConnectedException 
129         */
130        public DiscoverInfo discoverInfo() throws NoResponseException, XMPPErrorException, NotConnectedException
131        {
132                DiscoverInfo info = new DiscoverInfo();
133                info.setTo(to);
134                info.setNode(getId());
135                return (DiscoverInfo) con.createPacketCollectorAndSend(info).nextResultOrThrow();
136        }
137        
138        /**
139         * Get the subscriptions currently associated with this node.
140         * 
141         * @return List of {@link Subscription}
142         * @throws XMPPErrorException 
143         * @throws NoResponseException 
144         * @throws NotConnectedException 
145         * 
146         */
147        public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException
148        {
149        return getSubscriptions(null, null);
150        }
151
152    /**
153     * Get the subscriptions currently associated with this node.
154     * <p>
155     * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension.
156     * {@code returnedExtensions} will be filled with the packet extensions found in the answer.
157     * </p>
158     *
159     * @param additionalExtensions
160     * @param returnedExtensions a collection that will be filled with the returned packet
161     *        extensions
162     * @return List of {@link Subscription}
163     * @throws NoResponseException
164     * @throws XMPPErrorException
165     * @throws NotConnectedException
166     */
167    public List<Subscription> getSubscriptions(List<PacketExtension> additionalExtensions, Collection<PacketExtension> returnedExtensions)
168                    throws NoResponseException, XMPPErrorException, NotConnectedException {
169        PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension(
170                        PubSubElementType.SUBSCRIPTIONS, getId()));
171        if (additionalExtensions != null) {
172            for (PacketExtension pe : additionalExtensions) {
173                pubSub.addExtension(pe);
174            }
175        }
176        PubSub reply = (PubSub) sendPubsubPacket(pubSub);
177        if (returnedExtensions != null) {
178            returnedExtensions.addAll(reply.getExtensions());
179        }
180        SubscriptionsExtension subElem = (SubscriptionsExtension) reply.getExtension(PubSubElementType.SUBSCRIPTIONS);
181        return subElem.getSubscriptions();
182    }
183
184        /**
185         * Get the affiliations of this node.
186         *
187         * @return List of {@link Affiliation}
188         * @throws NoResponseException
189         * @throws XMPPErrorException
190         * @throws NotConnectedException
191         */
192    public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException,
193                    NotConnectedException {
194        return getAffiliations(null, null);
195    }
196
197    /**
198     * Get the affiliations of this node.
199     * <p>
200     * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension.
201     * {@code returnedExtensions} will be filled with the packet extensions found in the answer.
202     * </p>
203     *
204     * @param additionalExtensions additional {@code PacketExtensions} add to the request
205     * @param returnedExtensions a collection that will be filled with the returned packet
206     *        extensions
207     * @return List of {@link Affiliation}
208     * @throws NoResponseException
209     * @throws XMPPErrorException
210     * @throws NotConnectedException
211     */
212    public List<Affiliation> getAffiliations(List<PacketExtension> additionalExtensions, Collection<PacketExtension> returnedExtensions)
213                    throws NoResponseException, XMPPErrorException, NotConnectedException {
214        PubSub pubSub = createPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.AFFILIATIONS, getId()));
215        if (additionalExtensions != null) {
216            for (PacketExtension pe : additionalExtensions) {
217                pubSub.addExtension(pe);
218            }
219        }
220        PubSub reply = (PubSub) sendPubsubPacket(pubSub);
221        if (returnedExtensions != null) {
222            returnedExtensions.addAll(reply.getExtensions());
223        }
224        AffiliationsExtension affilElem = (AffiliationsExtension) reply.getExtension(PubSubElementType.AFFILIATIONS);
225        return affilElem.getAffiliations();
226    }
227
228        /**
229         * The user subscribes to the node using the supplied jid.  The
230         * bare jid portion of this one must match the jid for the connection.
231         * 
232         * Please note that the {@link Subscription.State} should be checked 
233         * on return since more actions may be required by the caller.
234         * {@link Subscription.State#pending} - The owner must approve the subscription 
235         * request before messages will be received.
236         * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 
237         * the caller must configure the subscription before messages will be received.  If it is false
238         * the caller can configure it but is not required to do so.
239         * @param jid The jid to subscribe as.
240         * @return The subscription
241         * @throws XMPPErrorException 
242         * @throws NoResponseException 
243         * @throws NotConnectedException 
244         */
245        public Subscription subscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException
246        {
247            PubSub pubSub = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
248                PubSub reply = (PubSub)sendPubsubPacket(pubSub);
249                return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
250        }
251        
252        /**
253         * The user subscribes to the node using the supplied jid and subscription
254         * options.  The bare jid portion of this one must match the jid for the 
255         * connection.
256         * 
257         * Please note that the {@link Subscription.State} should be checked 
258         * on return since more actions may be required by the caller.
259         * {@link Subscription.State#pending} - The owner must approve the subscription 
260         * request before messages will be received.
261         * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 
262         * the caller must configure the subscription before messages will be received.  If it is false
263         * the caller can configure it but is not required to do so.
264         * @param jid The jid to subscribe as.
265         * @return The subscription
266         * @throws XMPPErrorException 
267         * @throws NoResponseException 
268         * @throws NotConnectedException 
269         */
270        public Subscription subscribe(String jid, SubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException
271        {
272            PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
273                request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
274                PubSub reply = (PubSub)PubSubManager.sendPubsubPacket(con, request);
275                return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
276        }
277
278        /**
279         * Remove the subscription related to the specified JID.  This will only 
280         * work if there is only 1 subscription.  If there are multiple subscriptions,
281         * use {@link #unsubscribe(String, String)}.
282         * 
283         * @param jid The JID used to subscribe to the node
284         * @throws XMPPErrorException 
285         * @throws NoResponseException 
286         * @throws NotConnectedException 
287         * 
288         */
289        public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException
290        {
291                unsubscribe(jid, null);
292        }
293        
294        /**
295         * Remove the specific subscription related to the specified JID.
296         * 
297         * @param jid The JID used to subscribe to the node
298         * @param subscriptionId The id of the subscription being removed
299         * @throws XMPPErrorException 
300         * @throws NoResponseException 
301         * @throws NotConnectedException 
302         */
303        public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException
304        {
305                sendPubsubPacket(createPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(), subscriptionId)));
306        }
307
308        /**
309         * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
310         * via the {@link #sendConfigurationForm(Form)}.
311         * 
312         * @return A subscription options form
313         * @throws XMPPErrorException 
314         * @throws NoResponseException 
315         * @throws NotConnectedException 
316         */
317        public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException
318        {
319                return getSubscriptionOptions(jid, null);
320        }
321
322
323        /**
324         * Get the options for configuring the specified subscription.
325         * 
326         * @param jid JID the subscription is registered under
327         * @param subscriptionId The subscription id
328         * 
329         * @return The subscription option form
330         * @throws XMPPErrorException 
331         * @throws NoResponseException 
332         * @throws NotConnectedException 
333         * 
334         */
335        public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException
336        {
337                PubSub packet = (PubSub)sendPubsubPacket(createPubsubPacket(Type.GET, new OptionsExtension(jid, getId(), subscriptionId)));
338                FormNode ext = (FormNode)packet.getExtension(PubSubElementType.OPTIONS);
339                return new SubscribeForm(ext.getForm());
340        }
341
342        /**
343         * Register a listener for item publication events.  This 
344         * listener will get called whenever an item is published to 
345         * this node.
346         * 
347         * @param listener The handler for the event
348         */
349        @SuppressWarnings("unchecked")
350    public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener)
351        {
352                PacketListener conListener = new ItemEventTranslator(listener); 
353                itemEventToListenerMap.put(listener, conListener);
354                con.addPacketListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));
355        }
356
357        /**
358         * Unregister a listener for publication events.
359         * 
360         * @param listener The handler to unregister
361         */
362        public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener)
363        {
364                PacketListener conListener = itemEventToListenerMap.remove(listener);
365                
366                if (conListener != null)
367                        con.removePacketListener(conListener);
368        }
369
370        /**
371         * Register a listener for configuration events.  This listener
372         * will get called whenever the node's configuration changes.
373         * 
374         * @param listener The handler for the event
375         */
376        public void addConfigurationListener(NodeConfigListener listener)
377        {
378                PacketListener conListener = new NodeConfigTranslator(listener); 
379                configEventToListenerMap.put(listener, conListener);
380                con.addPacketListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));
381        }
382
383        /**
384         * Unregister a listener for configuration events.
385         * 
386         * @param listener The handler to unregister
387         */
388        public void removeConfigurationListener(NodeConfigListener listener)
389        {
390                PacketListener conListener = configEventToListenerMap .remove(listener);
391                
392                if (conListener != null)
393                        con.removePacketListener(conListener);
394        }
395        
396        /**
397         * Register an listener for item delete events.  This listener
398         * gets called whenever an item is deleted from the node.
399         * 
400         * @param listener The handler for the event
401         */
402        public void addItemDeleteListener(ItemDeleteListener listener)
403        {
404                PacketListener delListener = new ItemDeleteTranslator(listener); 
405                itemDeleteToListenerMap.put(listener, delListener);
406                EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");
407                EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());
408                
409                con.addPacketListener(delListener, new OrFilter(deleteItem, purge));
410        }
411
412        /**
413         * Unregister a listener for item delete events.
414         * 
415         * @param listener The handler to unregister
416         */
417        public void removeItemDeleteListener(ItemDeleteListener listener)
418        {
419                PacketListener conListener = itemDeleteToListenerMap .remove(listener);
420                
421                if (conListener != null)
422                        con.removePacketListener(conListener);
423        }
424
425        @Override
426        public String toString()
427        {
428                return super.toString() + " " + getClass().getName() + " id: " + id;
429        }
430        
431        protected PubSub createPubsubPacket(Type type, PacketExtension ext)
432        {
433                return createPubsubPacket(type, ext, null);
434        }
435        
436        protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)
437        {
438                return PubSub.createPubsubPacket(to, type, ext, ns);
439        }
440
441        protected Packet sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException
442        {
443                return PubSubManager.sendPubsubPacket(con, packet);
444        }
445
446
447        private static List<String> getSubscriptionIds(Packet packet)
448        {
449                HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim");
450                List<String> values = null;
451                
452                if (headers != null)
453                {
454                        values = new ArrayList<String>(headers.getHeaders().size());
455                        
456                        for (Header header : headers.getHeaders())
457                        {
458                                values.add(header.getValue());
459                        }
460                }
461                return values;
462        }
463
464        /**
465         * This class translates low level item publication events into api level objects for 
466         * user consumption.
467         * 
468         * @author Robin Collier
469         */
470        public class ItemEventTranslator implements PacketListener
471        {
472                @SuppressWarnings("rawtypes")
473        private ItemEventListener listener;
474
475                public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener)
476                {
477                        listener = eventListener;
478                }
479                
480                @SuppressWarnings({ "rawtypes", "unchecked" })
481        public void processPacket(Packet packet)
482                {
483                EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
484                        ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
485                        DelayInformation delay = (DelayInformation)packet.getExtension("delay", "urn:xmpp:delay");
486                        
487                        // If there was no delay based on XEP-0203, then try XEP-0091 for backward compatibility
488                        if (delay == null)
489                        {
490                                delay = (DelayInformation)packet.getExtension("x", "jabber:x:delay");
491                        }
492            ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), (delay == null ? null : delay.getStamp()));
493                        listener.handlePublishedItems(eventItems);
494                }
495        }
496
497        /**
498         * This class translates low level item deletion events into api level objects for 
499         * user consumption.
500         * 
501         * @author Robin Collier
502         */
503        public class ItemDeleteTranslator implements PacketListener
504        {
505                private ItemDeleteListener listener;
506
507                public ItemDeleteTranslator(ItemDeleteListener eventListener)
508                {
509                        listener = eventListener;
510                }
511                
512                public void processPacket(Packet packet)
513                {
514                EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
515                
516                List<PacketExtension> extList = event.getExtensions();
517                
518                if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName()))
519                {
520                        listener.handlePurge();
521                }
522                else
523                {
524                                ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
525                                @SuppressWarnings("unchecked")
526                Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems();
527                                List<String> items = new ArrayList<String>(pubItems.size());
528
529                                for (RetractItem item : pubItems)
530                                {
531                                        items.add(item.getId());
532                                }
533
534                                ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));
535                                listener.handleDeletedItems(eventItems);
536                }
537                }
538        }
539        
540        /**
541         * This class translates low level node configuration events into api level objects for 
542         * user consumption.
543         * 
544         * @author Robin Collier
545         */
546        public class NodeConfigTranslator implements PacketListener
547        {
548                private NodeConfigListener listener;
549
550                public NodeConfigTranslator(NodeConfigListener eventListener)
551                {
552                        listener = eventListener;
553                }
554                
555                public void processPacket(Packet packet)
556                {
557                EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
558                        ConfigurationEvent config = (ConfigurationEvent)event.getEvent();
559
560                        listener.handleNodeConfiguration(config);
561                }
562        }
563
564        /**
565         * Filter for {@link PacketListener} to filter out events not specific to the 
566         * event type expected for this node.
567         * 
568         * @author Robin Collier
569         */
570        class EventContentFilter implements PacketFilter
571        {
572                private String firstElement;
573                private String secondElement;
574                
575                EventContentFilter(String elementName)
576                {
577                        firstElement = elementName;
578                }
579
580                EventContentFilter(String firstLevelEelement, String secondLevelElement)
581                {
582                        firstElement = firstLevelEelement;
583                        secondElement = secondLevelElement;
584                }
585
586                public boolean accept(Packet packet)
587                {
588                        if (!(packet instanceof Message))
589                                return false;
590
591                        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
592                        
593                        if (event == null)
594                                return false;
595
596                        NodeExtension embedEvent = event.getEvent();
597                        
598                        if (embedEvent == null)
599                                return false;
600                        
601                        if (embedEvent.getElementName().equals(firstElement))
602                        {
603                                if (!embedEvent.getNode().equals(getId()))
604                                        return false;
605                                
606                                if (secondElement == null)
607                                        return true;
608                                
609                                if (embedEvent instanceof EmbeddedPacketExtension)
610                                {
611                                        List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions();
612                                        
613                                        if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))
614                                                return true;
615                                }
616                        }
617                        return false;
618                }
619        }
620}