Node.java

  1. /**
  2.  *
  3.  * Copyright 2009 Robin Collier.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.pubsub;

  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.List;
  21. import java.util.concurrent.ConcurrentHashMap;

  22. import org.jivesoftware.smack.SmackException.NoResponseException;
  23. import org.jivesoftware.smack.SmackException.NotConnectedException;
  24. import org.jivesoftware.smack.StanzaListener;
  25. import org.jivesoftware.smack.XMPPConnection;
  26. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  27. import org.jivesoftware.smack.filter.FlexibleStanzaTypeFilter;
  28. import org.jivesoftware.smack.filter.OrFilter;
  29. import org.jivesoftware.smack.packet.IQ;
  30. import org.jivesoftware.smack.packet.Message;
  31. import org.jivesoftware.smack.packet.Stanza;
  32. import org.jivesoftware.smack.packet.XmlElement;

  33. import org.jivesoftware.smackx.delay.DelayInformationManager;
  34. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  35. import org.jivesoftware.smackx.pubsub.Affiliation.AffiliationNamespace;
  36. import org.jivesoftware.smackx.pubsub.SubscriptionsExtension.SubscriptionsNamespace;
  37. import org.jivesoftware.smackx.pubsub.form.ConfigureForm;
  38. import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm;
  39. import org.jivesoftware.smackx.pubsub.form.FillableSubscribeForm;
  40. import org.jivesoftware.smackx.pubsub.form.SubscribeForm;
  41. import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
  42. import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
  43. import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
  44. import org.jivesoftware.smackx.pubsub.packet.PubSub;
  45. import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
  46. import org.jivesoftware.smackx.pubsub.util.NodeUtils;
  47. import org.jivesoftware.smackx.shim.packet.Header;
  48. import org.jivesoftware.smackx.shim.packet.HeadersExtension;
  49. import org.jivesoftware.smackx.xdata.packet.DataForm;

  50. import org.jxmpp.jid.Jid;

  51. public abstract class Node {
  52.     protected final PubSubManager pubSubManager;
  53.     protected final String id;

  54.     protected ConcurrentHashMap<ItemEventListener<Item>, StanzaListener> itemEventToListenerMap = new ConcurrentHashMap<>();
  55.     protected ConcurrentHashMap<ItemDeleteListener, StanzaListener> itemDeleteToListenerMap = new ConcurrentHashMap<>();
  56.     protected ConcurrentHashMap<NodeConfigListener, StanzaListener> configEventToListenerMap = new ConcurrentHashMap<>();

  57.     /**
  58.      * Construct a node associated to the supplied connection with the specified
  59.      * node id.
  60.      *
  61.      * @param pubSubManager The PubSubManager for the connection the node is associated with
  62.      * @param nodeId The node id
  63.      */
  64.     Node(PubSubManager pubSubManager, String nodeId) {
  65.         this.pubSubManager = pubSubManager;
  66.         id = nodeId;
  67.     }

  68.     /**
  69.      * Get the NodeId.
  70.      *
  71.      * @return the node id
  72.      */
  73.     public String getId() {
  74.         return id;
  75.     }
  76.     /**
  77.      * Returns a configuration form, from which you can create an answer form to be submitted
  78.      * via the {@link #sendConfigurationForm(FillableConfigureForm)}.
  79.      *
  80.      * @return the configuration form
  81.      * @throws XMPPErrorException if there was an XMPP error returned.
  82.      * @throws NoResponseException if there was no response from the remote entity.
  83.      * @throws NotConnectedException if the XMPP connection is not connected.
  84.      * @throws InterruptedException if the calling thread was interrupted.
  85.      */
  86.     public ConfigureForm getNodeConfiguration() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  87.         PubSub pubSub = createPubsubPacket(IQ.Type.get, new NodeExtension(
  88.                         PubSubElementType.CONFIGURE_OWNER, getId()));
  89.         Stanza reply = sendPubsubPacket(pubSub);
  90.         return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER);
  91.     }

  92.     /**
  93.      * Update the configuration with the contents of the new {@link FillableConfigureForm}.
  94.      *
  95.      * @param configureForm the filled node configuration form with the nodes new configuration.
  96.      * @throws XMPPErrorException if there was an XMPP error returned.
  97.      * @throws NoResponseException if there was no response from the remote entity.
  98.      * @throws NotConnectedException if the XMPP connection is not connected.
  99.      * @throws InterruptedException if the calling thread was interrupted.
  100.      */
  101.     public void sendConfigurationForm(FillableConfigureForm configureForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  102.         PubSub packet = createPubsubPacket(IQ.Type.set, new FormNode(FormNodeType.CONFIGURE_OWNER,
  103.                         getId(), configureForm.getDataFormToSubmit()));
  104.         pubSubManager.getConnection().sendIqRequestAndWaitForResponse(packet);
  105.     }

  106.     /**
  107.      * Discover node information in standard {@link DiscoverInfo} format.
  108.      *
  109.      * @return The discovery information about the node.
  110.      * @throws XMPPErrorException if there was an XMPP error returned.
  111.      * @throws NoResponseException if there was no response from the server.
  112.      * @throws NotConnectedException if the XMPP connection is not connected.
  113.      * @throws InterruptedException if the calling thread was interrupted.
  114.      */
  115.     public DiscoverInfo discoverInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  116.         XMPPConnection connection = pubSubManager.getConnection();
  117.         DiscoverInfo discoverInfoRequest = DiscoverInfo.builder(connection)
  118.                 .to(pubSubManager.getServiceJid())
  119.                 .setNode(getId())
  120.                 .build();
  121.         return connection.sendIqRequestAndWaitForResponse(discoverInfoRequest);
  122.     }

  123.     /**
  124.      * Get the subscriptions currently associated with this node.
  125.      *
  126.      * @return List of {@link Subscription}
  127.      * @throws XMPPErrorException if there was an XMPP error returned.
  128.      * @throws NoResponseException if there was no response from the remote entity.
  129.      * @throws NotConnectedException if the XMPP connection is not connected.
  130.      * @throws InterruptedException if the calling thread was interrupted.
  131.      *
  132.      */
  133.     public List<Subscription> getSubscriptions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  134.         return getSubscriptions(null, null);
  135.     }

  136.     /**
  137.      * Get the subscriptions currently associated with this node.
  138.      * <p>
  139.      * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension.
  140.      * {@code returnedExtensions} will be filled with the stanza extensions found in the answer.
  141.      * </p>
  142.      *
  143.      * @param additionalExtensions TODO javadoc me please
  144.      * @param returnedExtensions a collection that will be filled with the returned packet
  145.      *        extensions
  146.      * @return List of {@link Subscription}
  147.      * @throws NoResponseException if there was no response from the remote entity.
  148.      * @throws XMPPErrorException if there was an XMPP error returned.
  149.      * @throws NotConnectedException if the XMPP connection is not connected.
  150.      * @throws InterruptedException if the calling thread was interrupted.
  151.      */
  152.     public List<Subscription> getSubscriptions(List<XmlElement> additionalExtensions, Collection<XmlElement> returnedExtensions)
  153.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  154.         return getSubscriptions(SubscriptionsNamespace.basic, additionalExtensions, returnedExtensions);
  155.     }

  156.     /**
  157.      * Get the subscriptions currently associated with this node as owner.
  158.      *
  159.      * @return List of {@link Subscription}
  160.      * @throws XMPPErrorException if there was an XMPP error returned.
  161.      * @throws NoResponseException if there was no response from the remote entity.
  162.      * @throws NotConnectedException if the XMPP connection is not connected.
  163.      * @throws InterruptedException if the calling thread was interrupted.
  164.      * @see #getSubscriptionsAsOwner(List, Collection)
  165.      * @since 4.1
  166.      */
  167.     public List<Subscription> getSubscriptionsAsOwner() throws NoResponseException, XMPPErrorException,
  168.                     NotConnectedException, InterruptedException {
  169.         return getSubscriptionsAsOwner(null, null);
  170.     }

  171.     /**
  172.      * Get the subscriptions currently associated with this node as owner.
  173.      * <p>
  174.      * Unlike {@link #getSubscriptions(List, Collection)}, which only retrieves the subscriptions of the current entity
  175.      * ("user"), this method returns a list of <b>all</b> subscriptions. This requires the entity to have the sufficient
  176.      * privileges to manage subscriptions.
  177.      * </p>
  178.      * <p>
  179.      * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension.
  180.      * {@code returnedExtensions} will be filled with the stanza extensions found in the answer.
  181.      * </p>
  182.      *
  183.      * @param additionalExtensions TODO javadoc me please
  184.      * @param returnedExtensions a collection that will be filled with the returned stanza extensions
  185.      * @return List of {@link Subscription}
  186.      * @throws NoResponseException if there was no response from the remote entity.
  187.      * @throws XMPPErrorException if there was an XMPP error returned.
  188.      * @throws NotConnectedException if the XMPP connection is not connected.
  189.      * @throws InterruptedException if the calling thread was interrupted.
  190.      * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-subscriptions-retrieve">XEP-60 § 8.8.1 -
  191.      *      Retrieve Subscriptions List</a>
  192.      * @since 4.1
  193.      */
  194.     public List<Subscription> getSubscriptionsAsOwner(List<XmlElement> additionalExtensions,
  195.                     Collection<XmlElement> returnedExtensions) throws NoResponseException, XMPPErrorException,
  196.                     NotConnectedException, InterruptedException {
  197.         return getSubscriptions(SubscriptionsNamespace.owner, additionalExtensions, returnedExtensions);
  198.     }

  199.     private List<Subscription> getSubscriptions(SubscriptionsNamespace subscriptionsNamespace, List<XmlElement> additionalExtensions,
  200.                     Collection<XmlElement> returnedExtensions)
  201.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  202.         PubSubElementType pubSubElementType = subscriptionsNamespace.type;

  203.         PubSub pubSub = createPubsubPacket(IQ.Type.get, new NodeExtension(pubSubElementType, getId()));
  204.         if (additionalExtensions != null) {
  205.             for (XmlElement pe : additionalExtensions) {
  206.                 pubSub.addExtension(pe);
  207.             }
  208.         }
  209.         PubSub reply = sendPubsubPacket(pubSub);
  210.         if (returnedExtensions != null) {
  211.             returnedExtensions.addAll(reply.getExtensions());
  212.         }
  213.         SubscriptionsExtension subElem = reply.getExtension(pubSubElementType);
  214.         return subElem.getSubscriptions();
  215.     }

  216.     /**
  217.      * Modify the subscriptions for this PubSub node as owner.
  218.      * <p>
  219.      * Note that the subscriptions are _not_ checked against the existing subscriptions
  220.      * since these are not cached (and indeed could change asynchronously)
  221.      * </p>
  222.      *
  223.      * @param changedSubs subscriptions that have changed
  224.      * @return <code>null</code> or a PubSub stanza with additional information on success.
  225.      * @throws NoResponseException if there was no response from the remote entity.
  226.      * @throws XMPPErrorException if there was an XMPP error returned.
  227.      * @throws NotConnectedException if the XMPP connection is not connected.
  228.      * @throws InterruptedException if the calling thread was interrupted.
  229.      * @see <a href="https://xmpp.org/extensions/xep-0060.html#owner-subscriptions-modify">XEP-60 § 8.8.2 Modify Subscriptions</a>
  230.      * @since 4.3
  231.      */
  232.     public PubSub modifySubscriptionsAsOwner(List<Subscription> changedSubs)
  233.         throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {

  234.         PubSub pubSub = createPubsubPacket(IQ.Type.set,
  235.             new SubscriptionsExtension(SubscriptionsNamespace.owner, getId(), changedSubs));
  236.         return sendPubsubPacket(pubSub);
  237.     }

  238.     /**
  239.      * Get the affiliations of this node.
  240.      *
  241.      * @return List of {@link Affiliation}
  242.      * @throws NoResponseException if there was no response from the remote entity.
  243.      * @throws XMPPErrorException if there was an XMPP error returned.
  244.      * @throws NotConnectedException if the XMPP connection is not connected.
  245.      * @throws InterruptedException if the calling thread was interrupted.
  246.      */
  247.     public List<Affiliation> getAffiliations() throws NoResponseException, XMPPErrorException,
  248.                     NotConnectedException, InterruptedException {
  249.         return getAffiliations(null, null);
  250.     }

  251.     /**
  252.      * Get the affiliations of this node.
  253.      * <p>
  254.      * {@code additionalExtensions} can be used e.g. to add a "Result Set Management" extension.
  255.      * {@code returnedExtensions} will be filled with the stanza extensions found in the answer.
  256.      * </p>
  257.      *
  258.      * @param additionalExtensions additional {@code PacketExtensions} add to the request
  259.      * @param returnedExtensions a collection that will be filled with the returned packet
  260.      *        extensions
  261.      * @return List of {@link Affiliation}
  262.      * @throws NoResponseException if there was no response from the remote entity.
  263.      * @throws XMPPErrorException if there was an XMPP error returned.
  264.      * @throws NotConnectedException if the XMPP connection is not connected.
  265.      * @throws InterruptedException if the calling thread was interrupted.
  266.      */
  267.     public List<Affiliation> getAffiliations(List<XmlElement> additionalExtensions, Collection<XmlElement> returnedExtensions)
  268.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {

  269.         return getAffiliations(AffiliationNamespace.basic, additionalExtensions, returnedExtensions);
  270.     }

  271.     /**
  272.      * Retrieve the affiliation list for this node as owner.
  273.      *
  274.      * @return list of entities whose affiliation is not 'none'.
  275.      * @throws NoResponseException if there was no response from the remote entity.
  276.      * @throws XMPPErrorException if there was an XMPP error returned.
  277.      * @throws NotConnectedException if the XMPP connection is not connected.
  278.      * @throws InterruptedException if the calling thread was interrupted.
  279.      * @see #getAffiliations(List, Collection)
  280.      * @since 4.2
  281.      */
  282.     public List<Affiliation> getAffiliationsAsOwner()
  283.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {

  284.         return getAffiliationsAsOwner(null, null);
  285.     }

  286.     /**
  287.      * Retrieve the affiliation list for this node as owner.
  288.      * <p>
  289.      * Note that this is an <b>optional</b> PubSub feature ('pubsub#modify-affiliations').
  290.      * </p>
  291.      *
  292.      * @param additionalExtensions optional additional extension elements add to the request.
  293.      * @param returnedExtensions an optional collection that will be filled with the returned
  294.      *        extension elements.
  295.      * @return list of entities whose affiliation is not 'none'.
  296.      * @throws NoResponseException if there was no response from the remote entity.
  297.      * @throws XMPPErrorException if there was an XMPP error returned.
  298.      * @throws NotConnectedException if the XMPP connection is not connected.
  299.      * @throws InterruptedException if the calling thread was interrupted.
  300.      * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-affiliations-retrieve">XEP-60 § 8.9.1 Retrieve Affiliations List</a>
  301.      * @since 4.2
  302.      */
  303.     public List<Affiliation> getAffiliationsAsOwner(List<XmlElement> additionalExtensions, Collection<XmlElement> returnedExtensions)
  304.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {

  305.         return getAffiliations(AffiliationNamespace.owner, additionalExtensions, returnedExtensions);
  306.     }

  307.     private List<Affiliation> getAffiliations(AffiliationNamespace affiliationsNamespace, List<XmlElement> additionalExtensions,
  308.                     Collection<XmlElement> returnedExtensions) throws NoResponseException, XMPPErrorException,
  309.                     NotConnectedException, InterruptedException {
  310.         PubSubElementType pubSubElementType = affiliationsNamespace.type;

  311.         PubSub pubSub = createPubsubPacket(IQ.Type.get, new NodeExtension(pubSubElementType, getId()));
  312.         if (additionalExtensions != null) {
  313.             for (XmlElement pe : additionalExtensions) {
  314.                 pubSub.addExtension(pe);
  315.             }
  316.         }
  317.         PubSub reply = sendPubsubPacket(pubSub);
  318.         if (returnedExtensions != null) {
  319.             returnedExtensions.addAll(reply.getExtensions());
  320.         }
  321.         AffiliationsExtension affilElem = reply.getExtension(pubSubElementType);
  322.         return affilElem.getAffiliations();
  323.     }

  324.     /**
  325.      * Modify the affiliations for this PubSub node as owner. The {@link Affiliation}s given must be created with the
  326.      * {@link Affiliation#Affiliation(org.jxmpp.jid.BareJid, Affiliation.Type)} constructor.
  327.      * <p>
  328.      * Note that this is an <b>optional</b> PubSub feature ('pubsub#modify-affiliations').
  329.      * </p>
  330.      *
  331.      * @param affiliations TODO javadoc me please
  332.      * @return <code>null</code> or a PubSub stanza with additional information on success.
  333.      * @throws NoResponseException if there was no response from the remote entity.
  334.      * @throws XMPPErrorException if there was an XMPP error returned.
  335.      * @throws NotConnectedException if the XMPP connection is not connected.
  336.      * @throws InterruptedException if the calling thread was interrupted.
  337.      * @see <a href="http://www.xmpp.org/extensions/xep-0060.html#owner-affiliations-modify">XEP-60 § 8.9.2 Modify Affiliation</a>
  338.      * @since 4.2
  339.      */
  340.     public PubSub modifyAffiliationAsOwner(List<Affiliation> affiliations) throws NoResponseException,
  341.                     XMPPErrorException, NotConnectedException, InterruptedException {
  342.         for (Affiliation affiliation : affiliations) {
  343.             if (affiliation.getPubSubNamespace() != PubSubNamespace.owner) {
  344.                 throw new IllegalArgumentException("Must use Affiliation(BareJid, Type) affiliations");
  345.             }
  346.         }

  347.         PubSub pubSub = createPubsubPacket(IQ.Type.set, new AffiliationsExtension(AffiliationNamespace.owner, affiliations, getId()));
  348.         return sendPubsubPacket(pubSub);
  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 if there was an XMPP error returned.
  364.      * @throws NoResponseException if there was no response from the remote entity.
  365.      * @throws NotConnectedException if the XMPP connection is not connected.
  366.      * @throws InterruptedException if the calling thread was interrupted.
  367.      */
  368.     public Subscription subscribe(Jid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  369.         PubSub pubSub = createPubsubPacket(IQ.Type.set, new SubscribeExtension(jid, getId()));
  370.         PubSub reply = sendPubsubPacket(pubSub);
  371.         return reply.getExtension(PubSubElementType.SUBSCRIPTION);
  372.     }

  373.     /**
  374.      * The user subscribes to the node using the supplied jid and subscription
  375.      * options.  The bare jid portion of this one must match the jid for the
  376.      * connection.
  377.      *
  378.      * Please note that the {@link Subscription.State} should be checked
  379.      * on return since more actions may be required by the caller.
  380.      * {@link Subscription.State#pending} - The owner must approve the subscription
  381.      * request before messages will be received.
  382.      * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true,
  383.      * the caller must configure the subscription before messages will be received.  If it is false
  384.      * the caller can configure it but is not required to do so.
  385.      *
  386.      * @param jid The jid to subscribe as.
  387.      * @param subForm TODO javadoc me please
  388.      *
  389.      * @return The subscription
  390.      * @throws XMPPErrorException if there was an XMPP error returned.
  391.      * @throws NoResponseException if there was no response from the remote entity.
  392.      * @throws NotConnectedException if the XMPP connection is not connected.
  393.      * @throws InterruptedException if the calling thread was interrupted.
  394.      */
  395.     public Subscription subscribe(Jid jid, FillableSubscribeForm subForm) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  396.         DataForm submitForm = subForm.getDataFormToSubmit();
  397.         PubSub request = createPubsubPacket(IQ.Type.set, new SubscribeExtension(jid, getId()));
  398.         request.addExtension(new FormNode(FormNodeType.OPTIONS, submitForm));
  399.         PubSub reply = sendPubsubPacket(request);
  400.         return reply.getExtension(PubSubElementType.SUBSCRIPTION);
  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 if there was an XMPP error returned.
  409.      * @throws NoResponseException if there was no response from the remote entity.
  410.      * @throws NotConnectedException if the XMPP connection is not connected.
  411.      * @throws InterruptedException if the calling thread was interrupted.
  412.      *
  413.      */
  414.     public void unsubscribe(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  415.         unsubscribe(jid, null);
  416.     }

  417.     /**
  418.      * Remove the specific subscription related to the specified JID.
  419.      *
  420.      * @param jid The JID used to subscribe to the node
  421.      * @param subscriptionId The id of the subscription being removed
  422.      * @throws XMPPErrorException if there was an XMPP error returned.
  423.      * @throws NoResponseException if there was no response from the remote entity.
  424.      * @throws NotConnectedException if the XMPP connection is not connected.
  425.      * @throws InterruptedException if the calling thread was interrupted.
  426.      */
  427.     public void unsubscribe(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  428.         sendPubsubPacket(createPubsubPacket(IQ.Type.set, new UnsubscribeExtension(jid, getId(), subscriptionId)));
  429.     }

  430.     /**
  431.      * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
  432.      * via the {@link #sendConfigurationForm(FillableConfigureForm)}.
  433.      *
  434.      * @param jid TODO javadoc me please
  435.      *
  436.      * @return A subscription options form
  437.      * @throws XMPPErrorException if there was an XMPP error returned.
  438.      * @throws NoResponseException if there was no response from the remote entity.
  439.      * @throws NotConnectedException if the XMPP connection is not connected.
  440.      * @throws InterruptedException if the calling thread was interrupted.
  441.      */
  442.     public SubscribeForm getSubscriptionOptions(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  443.         return getSubscriptionOptions(jid, null);
  444.     }


  445.     /**
  446.      * Get the options for configuring the specified subscription.
  447.      *
  448.      * @param jid JID the subscription is registered under
  449.      * @param subscriptionId The subscription id
  450.      *
  451.      * @return The subscription option form
  452.      * @throws XMPPErrorException if there was an XMPP error returned.
  453.      * @throws NoResponseException if there was no response from the remote entity.
  454.      * @throws NotConnectedException if the XMPP connection is not connected.
  455.      * @throws InterruptedException if the calling thread was interrupted.
  456.      *
  457.      */
  458.     public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  459.         PubSub packet = sendPubsubPacket(createPubsubPacket(IQ.Type.get, new OptionsExtension(jid, getId(), subscriptionId)));
  460.         FormNode ext = packet.getExtension(PubSubElementType.OPTIONS);
  461.         return new SubscribeForm(ext.getForm());
  462.     }

  463.     /**
  464.      * Register a listener for item publication events.  This
  465.      * listener will get called whenever an item is published to
  466.      * this node.
  467.      *
  468.      * @param listener The handler for the event
  469.      */
  470.     @SuppressWarnings("unchecked")
  471.     public void addItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) {
  472.         StanzaListener conListener = new ItemEventTranslator(listener);
  473.         itemEventToListenerMap.put(listener, conListener);
  474.         pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));
  475.     }

  476.     /**
  477.      * Unregister a listener for publication events.
  478.      *
  479.      * @param listener The handler to unregister
  480.      */
  481.     public void removeItemEventListener(@SuppressWarnings("rawtypes") ItemEventListener listener) {
  482.         StanzaListener conListener = itemEventToListenerMap.remove(listener);

  483.         if (conListener != null)
  484.             pubSubManager.getConnection().removeSyncStanzaListener(conListener);
  485.     }

  486.     /**
  487.      * Register a listener for configuration events.  This listener
  488.      * will get called whenever the node's configuration changes.
  489.      *
  490.      * @param listener The handler for the event
  491.      */
  492.     public void addConfigurationListener(NodeConfigListener listener) {
  493.         StanzaListener conListener = new NodeConfigTranslator(listener);
  494.         configEventToListenerMap.put(listener, conListener);
  495.         pubSubManager.getConnection().addSyncStanzaListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));
  496.     }

  497.     /**
  498.      * Unregister a listener for configuration events.
  499.      *
  500.      * @param listener The handler to unregister
  501.      */
  502.     public void removeConfigurationListener(NodeConfigListener listener) {
  503.         StanzaListener conListener = configEventToListenerMap .remove(listener);

  504.         if (conListener != null)
  505.             pubSubManager.getConnection().removeSyncStanzaListener(conListener);
  506.     }

  507.     /**
  508.      * Register an listener for item delete events.  This listener
  509.      * gets called whenever an item is deleted from the node.
  510.      *
  511.      * @param listener The handler for the event
  512.      */
  513.     public void addItemDeleteListener(ItemDeleteListener listener) {
  514.         StanzaListener delListener = new ItemDeleteTranslator(listener);
  515.         itemDeleteToListenerMap.put(listener, delListener);
  516.         EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");
  517.         EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());

  518.         // TODO: Use AsyncButOrdered (with Node as Key?)
  519.         pubSubManager.getConnection().addSyncStanzaListener(delListener, new OrFilter(deleteItem, purge));
  520.     }

  521.     /**
  522.      * Unregister a listener for item delete events.
  523.      *
  524.      * @param listener The handler to unregister
  525.      */
  526.     public void removeItemDeleteListener(ItemDeleteListener listener) {
  527.         StanzaListener conListener = itemDeleteToListenerMap .remove(listener);

  528.         if (conListener != null)
  529.             pubSubManager.getConnection().removeSyncStanzaListener(conListener);
  530.     }

  531.     @Override
  532.     public String toString() {
  533.         return super.toString() + " " + getClass().getName() + " id: " + id;
  534.     }

  535.     protected PubSub createPubsubPacket(IQ.Type type, NodeExtension ext) {
  536.         return PubSub.createPubsubPacket(pubSubManager.getServiceJid(), type, ext);
  537.     }

  538.     protected PubSub sendPubsubPacket(PubSub packet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  539.         return pubSubManager.sendPubsubPacket(packet);
  540.     }


  541.     private static List<String> getSubscriptionIds(Stanza packet) {
  542.         HeadersExtension headers = packet.getExtension(HeadersExtension.class);
  543.         List<String> values = null;

  544.         if (headers != null) {
  545.             values = new ArrayList<>(headers.getHeaders().size());

  546.             for (Header header : headers.getHeaders()) {
  547.                 values.add(header.getValue());
  548.             }
  549.         }
  550.         return values;
  551.     }

  552.     /**
  553.      * This class translates low level item publication events into api level objects for
  554.      * user consumption.
  555.      *
  556.      * @author Robin Collier
  557.      */
  558.     public static class ItemEventTranslator implements StanzaListener {
  559.         @SuppressWarnings("rawtypes")
  560.         private final ItemEventListener listener;

  561.         public ItemEventTranslator(@SuppressWarnings("rawtypes") ItemEventListener eventListener) {
  562.             listener = eventListener;
  563.         }

  564.         @Override
  565.         @SuppressWarnings({ "rawtypes", "unchecked" })
  566.         public void processStanza(Stanza packet) {
  567.             EventElement event = (EventElement) packet.getExtensionElement("event", PubSubNamespace.event.getXmlns());
  568.             ItemsExtension itemsElem = (ItemsExtension) event.getEvent();
  569.             ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), itemsElem.getItems(), getSubscriptionIds(packet), DelayInformationManager.getDelayTimestamp(packet));
  570.             // TODO: Use AsyncButOrdered (with Node as Key?)
  571.             listener.handlePublishedItems(eventItems);
  572.         }
  573.     }

  574.     /**
  575.      * This class translates low level item deletion events into api level objects for
  576.      * user consumption.
  577.      *
  578.      * @author Robin Collier
  579.      */
  580.     public static class ItemDeleteTranslator implements StanzaListener {
  581.         private final ItemDeleteListener listener;

  582.         public ItemDeleteTranslator(ItemDeleteListener eventListener) {
  583.             listener = eventListener;
  584.         }

  585.         @Override
  586.         public void processStanza(Stanza packet) {
  587. // CHECKSTYLE:OFF
  588.             EventElement event = (EventElement) packet.getExtensionElement("event", PubSubNamespace.event.getXmlns());

  589.             List<XmlElement> extList = event.getExtensions();

  590.             if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName())) {
  591.                 listener.handlePurge();
  592.             }
  593.             else {
  594.                 ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
  595.                 @SuppressWarnings("unchecked")
  596.                 Collection<RetractItem> pubItems = (Collection<RetractItem>) itemsElem.getItems();
  597.                 List<String> items = new ArrayList<>(pubItems.size());

  598.                 for (RetractItem item : pubItems) {
  599.                     items.add(item.getId());
  600.                 }

  601.                 ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));
  602.                 listener.handleDeletedItems(eventItems);
  603.             }
  604. // CHECKSTYLE:ON
  605.         }
  606.     }

  607.     /**
  608.      * This class translates low level node configuration events into api level objects for
  609.      * user consumption.
  610.      *
  611.      * @author Robin Collier
  612.      */
  613.     public static class NodeConfigTranslator implements StanzaListener {
  614.         private final NodeConfigListener listener;

  615.         public NodeConfigTranslator(NodeConfigListener eventListener) {
  616.             listener = eventListener;
  617.         }

  618.         @Override
  619.         public void processStanza(Stanza packet) {
  620.             EventElement event = (EventElement) packet.getExtensionElement("event", PubSubNamespace.event.getXmlns());
  621.             ConfigurationEvent config = (ConfigurationEvent) event.getEvent();

  622.             // TODO: Use AsyncButOrdered (with Node as Key?)
  623.             listener.handleNodeConfiguration(config);
  624.         }
  625.     }

  626.     /**
  627.      * Filter for {@link StanzaListener} to filter out events not specific to the
  628.      * event type expected for this node.
  629.      *
  630.      * @author Robin Collier
  631.      */
  632.     class EventContentFilter extends FlexibleStanzaTypeFilter<Message> {
  633.         private final String firstElement;
  634.         private final String secondElement;
  635.         private final boolean allowEmpty;

  636.         EventContentFilter(String elementName) {
  637.             this(elementName, null);
  638.         }

  639.         EventContentFilter(String firstLevelElement, String secondLevelElement) {
  640.             firstElement = firstLevelElement;
  641.             secondElement = secondLevelElement;
  642.             allowEmpty = firstElement.equals(EventElementType.items.toString())
  643.                             && "item".equals(secondLevelElement);
  644.         }

  645.         @Override
  646.         public boolean acceptSpecific(Message message) {
  647.             EventElement event = EventElement.from(message);

  648.             if (event == null)
  649.                 return false;

  650.             NodeExtension embedEvent = event.getEvent();

  651.             if (embedEvent == null)
  652.                 return false;

  653.             if (embedEvent.getElementName().equals(firstElement)) {
  654.                 if (!embedEvent.getNode().equals(getId()))
  655.                     return false;

  656.                 if (secondElement == null)
  657.                     return true;

  658.                 if (embedEvent instanceof EmbeddedPacketExtension) {
  659.                     List<XmlElement> secondLevelList = ((EmbeddedPacketExtension) embedEvent).getExtensions();

  660.                     // XEP-0060 allows no elements on second level for notifications. See schema or
  661.                     // for example § 4.3:
  662.                     // "although event notifications MUST include an empty <items/> element;"
  663.                     if (allowEmpty && secondLevelList.isEmpty()) {
  664.                         return true;
  665.                     }

  666.                     if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))
  667.                         return true;
  668.                 }
  669.             }
  670.             return false;
  671.         }
  672.     }
  673. }