PepManager.java

/**
 *
 * Copyright 2003-2007 Jive Software, 2015-2020 Florian Schmaus
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx.pep;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;

import org.jivesoftware.smack.AsyncButOrdered;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.jidtype.FromJidTypeFilter;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jivesoftware.smack.util.MultiMap;

import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.pubsub.EventElement;
import org.jivesoftware.smackx.pubsub.Item;
import org.jivesoftware.smackx.pubsub.ItemsExtension;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
import org.jivesoftware.smackx.pubsub.PubSubFeature;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.pubsub.filter.EventItemsExtensionFilter;

import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;

/**
 *
 * Manages Personal Event Publishing (XEP-163). A PEPManager provides a high level access to
 * PubSub personal events. It also provides an easy way
 * to hook up custom logic when events are received from another XMPP client through PEPListeners.
 *
 * Use example:
 *
 * <pre>
 *   PepManager pepManager = PepManager.getInstanceFor(smackConnection);
 *   pepManager.addPepListener(new PepListener() {
 *       public void eventReceived(EntityBareJid from, EventElement event, Message message) {
 *           LOGGER.debug("Event received: " + event);
 *       }
 *   });
 * </pre>
 *
 * @author Jeff Williams
 * @author Florian Schmaus
 */
public final class PepManager extends Manager {

    private static final Logger LOGGER = Logger.getLogger(PepManager.class.getName());

    private static final Map<XMPPConnection, PepManager> INSTANCES = new WeakHashMap<>();

    public static synchronized PepManager getInstanceFor(XMPPConnection connection) {
        PepManager pepManager = INSTANCES.get(connection);
        if (pepManager == null) {
            pepManager = new PepManager(connection);
            INSTANCES.put(connection, pepManager);
        }
        return pepManager;
    }

    // TODO: Ideally PepManager would re-use PubSubManager for this. But the functionality in PubSubManager does not yet
    // exist.
    private static final StanzaFilter PEP_EVENTS_FILTER = new AndFilter(
            MessageTypeFilter.NORMAL_OR_HEADLINE,
            FromJidTypeFilter.ENTITY_BARE_JID,
            EventItemsExtensionFilter.INSTANCE);

    private final Set<PepListener> pepListeners = new CopyOnWriteArraySet<>();

    private final AsyncButOrdered<EntityBareJid> asyncButOrdered = new AsyncButOrdered<>();

    private final ServiceDiscoveryManager serviceDiscoveryManager;

    private final PubSubManager pepPubSubManager;

    private final MultiMap<String, PepEventListenerCoupling<? extends ExtensionElement>> pepEventListeners = new MultiMap<>();

    private final Map<PepEventListener<?>, PepEventListenerCoupling<?>> listenerToCouplingMap = new HashMap<>();

    /**
     * Creates a new PEP exchange manager.
     *
     * @param connection an XMPPConnection which is used to send and receive messages.
     */
    private PepManager(XMPPConnection connection) {
        super(connection);

        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
        pepPubSubManager = PubSubManager.getInstanceFor(connection, null);

        StanzaListener packetListener = new StanzaListener() {
            @Override
            public void processStanza(Stanza stanza) {
                final Message message = (Message) stanza;
                final EventElement event = EventElement.from(stanza);
                assert event != null;
                final EntityBareJid from = message.getFrom().asEntityBareJidIfPossible();
                assert from != null;

                asyncButOrdered.performAsyncButOrdered(from, new Runnable() {
                    @Override
                    public void run() {
                        ItemsExtension itemsExtension = (ItemsExtension) event.getEvent();
                        String node = itemsExtension.getNode();

                        for (PepListener listener : pepListeners) {
                            listener.eventReceived(from, event, message);
                        }

                        List<PepEventListenerCoupling<? extends ExtensionElement>> nodeListeners;
                        synchronized (pepEventListeners) {
                            nodeListeners = pepEventListeners.getAll(node);
                            if (nodeListeners.isEmpty()) {
                                return;
                            }

                            // Make a copy of the list. Note that it is important to do this within the synchronized
                            // block.
                            nodeListeners = CollectionUtil.newListWith(nodeListeners);
                        }

                        for (PepEventListenerCoupling<? extends ExtensionElement> listener : nodeListeners) {
                            // TODO: Can there be more than one item?
                            List<? extends NamedElement> items = itemsExtension.getItems();
                            for (NamedElement namedElementItem : items) {
                                Item item = (Item) namedElementItem;
                                String id = item.getId();
                                @SuppressWarnings("unchecked")
                                PayloadItem<ExtensionElement> payloadItem = (PayloadItem<ExtensionElement>) item;
                                ExtensionElement payload = payloadItem.getPayload();

                                listener.invoke(from, payload, id, message);
                            }
                        }
                    }
                });
            }
        };
        // TODO Add filter to check if from supports PubSub as per xep163 2 2.4
        connection.addSyncStanzaListener(packetListener, PEP_EVENTS_FILTER);
    }

    private static final class PepEventListenerCoupling<E extends ExtensionElement> {
        private final String node;
        private final Class<E> extensionElementType;
        private final PepEventListener<E> pepEventListener;

        private PepEventListenerCoupling(String node, Class<E> extensionElementType,
                        PepEventListener<E> pepEventListener) {
            this.node = node;
            this.extensionElementType = extensionElementType;
            this.pepEventListener = pepEventListener;
        }

        private void invoke(EntityBareJid from, ExtensionElement payload, String id, Message carrierMessage) {
            if (!extensionElementType.isInstance(payload)) {
                LOGGER.warning("Ignoring " + payload + " from " + carrierMessage + " as it is not of type "
                                + extensionElementType);
                return;
            }

            E extensionElementPayload = extensionElementType.cast(payload);
            pepEventListener.onPepEvent(from, extensionElementPayload, id, carrierMessage);
        }
    }

    public <E extends ExtensionElement> boolean addPepEventListener(String node, Class<E> extensionElementType,
                    PepEventListener<E> pepEventListener) {
        PepEventListenerCoupling<E> pepEventListenerCoupling = new PepEventListenerCoupling<>(node,
                        extensionElementType, pepEventListener);

        synchronized (pepEventListeners) {
            if (listenerToCouplingMap.containsKey(pepEventListener)) {
                return false;
            }
            listenerToCouplingMap.put(pepEventListener, pepEventListenerCoupling);
            /*
             * TODO: Replace the above with the below using putIfAbsent() if Smack's minimum required Android SDK level
             * is 24 or higher. PepEventListenerCoupling<?> currentPepEventListenerCoupling =
             * listenerToCouplingMap.putIfAbsent(pepEventListener, pepEventListenerCoupling); if
             * (currentPepEventListenerCoupling != null) { return false; }
             */

            boolean listenerForNodeExisted = pepEventListeners.put(node, pepEventListenerCoupling);
            if (!listenerForNodeExisted) {
                serviceDiscoveryManager.addFeature(node + PubSubManager.PLUS_NOTIFY);
            }
        }
        return true;
    }

    public boolean removePepEventListener(PepEventListener<?> pepEventListener) {
        synchronized (pepEventListeners) {
            PepEventListenerCoupling<?> pepEventListenerCoupling = listenerToCouplingMap.remove(pepEventListener);
            if (pepEventListenerCoupling == null) {
                return false;
            }

            String node = pepEventListenerCoupling.node;

            boolean mappingExisted = pepEventListeners.removeOne(node, pepEventListenerCoupling);
            assert mappingExisted;

            if (!pepEventListeners.containsKey(pepEventListenerCoupling.node)) {
                // This was the last listener for the node. Remove the +notify feature.
                serviceDiscoveryManager.removeFeature(node + PubSubManager.PLUS_NOTIFY);
            }
        }

        return true;
    }

    public PubSubManager getPepPubSubManager() {
        return pepPubSubManager;
    }

    /**
     * Adds a listener to PEPs. The listener will be fired anytime PEP events are received from remote XMPP clients.
     *
     * @param pepListener a roster exchange listener.
     * @return true if pepListener was added.
     * @deprecated use {@link #addPepEventListener(String, Class, PepEventListener)} instead.
     */
    // TODO: Remove in Smack 4.5
    @Deprecated
    public boolean addPepListener(PepListener pepListener) {
        return pepListeners.add(pepListener);
    }

    /**
     * Removes a listener from PEP events.
     *
     * @param pepListener a roster exchange listener.
     * @return true, if pepListener was removed.
     * @deprecated use {@link #removePepEventListener(PepEventListener)} instead.
     */
    // TODO: Remove in Smack 4.5.
    @Deprecated
    public boolean removePepListener(PepListener pepListener) {
        return pepListeners.remove(pepListener);
    }

    /**
     * Publish an event.
     *
     * @param nodeId the ID of the node to publish on.
     * @param item the item to publish.
     * @return the leaf node the item was published on.
     * @throws NotConnectedException if the XMPP connection is not connected.
     * @throws InterruptedException if the calling thread was interrupted.
     * @throws XMPPErrorException if there was an XMPP error returned.
     * @throws NoResponseException if there was no response from the remote entity.
     * @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
     */
    public LeafNode publish(String nodeId, Item item) throws NotConnectedException, InterruptedException,
                    NoResponseException, XMPPErrorException, NotALeafNodeException {
        // PEP nodes are auto created if not existent. Hence Use PubSubManager.tryToPublishAndPossibleAutoCreate() here.
        return pepPubSubManager.tryToPublishAndPossibleAutoCreate(nodeId, item);
    }

    /**
     * XEP-163 5.
     */
    private static final PubSubFeature[] REQUIRED_FEATURES = new PubSubFeature[] {
        // @formatter:off
        PubSubFeature.auto_create,
        PubSubFeature.auto_subscribe,
        PubSubFeature.filtered_notifications,
        // @formatter:on
    };

    public boolean isSupported()
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        XMPPConnection connection = connection();
        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
        BareJid localBareJid = connection.getUser().asBareJid();
        return serviceDiscoveryManager.supportsFeatures(localBareJid, REQUIRED_FEATURES);
    }
}