001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2015-2018 Florian Schmaus
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 */
017
018package org.jivesoftware.smackx.pep;
019
020import java.util.Map;
021import java.util.Set;
022import java.util.WeakHashMap;
023import java.util.concurrent.CopyOnWriteArraySet;
024
025import org.jivesoftware.smack.AsyncButOrdered;
026import org.jivesoftware.smack.Manager;
027import org.jivesoftware.smack.SmackException.NoResponseException;
028import org.jivesoftware.smack.SmackException.NotConnectedException;
029import org.jivesoftware.smack.StanzaListener;
030import org.jivesoftware.smack.XMPPConnection;
031import org.jivesoftware.smack.XMPPException.XMPPErrorException;
032import org.jivesoftware.smack.filter.AndFilter;
033import org.jivesoftware.smack.filter.StanzaFilter;
034import org.jivesoftware.smack.filter.jidtype.AbstractJidTypeFilter.JidType;
035import org.jivesoftware.smack.filter.jidtype.FromJidTypeFilter;
036import org.jivesoftware.smack.packet.Message;
037import org.jivesoftware.smack.packet.Stanza;
038
039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
040import org.jivesoftware.smackx.pubsub.EventElement;
041import org.jivesoftware.smackx.pubsub.Item;
042import org.jivesoftware.smackx.pubsub.LeafNode;
043import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
044import org.jivesoftware.smackx.pubsub.PubSubFeature;
045import org.jivesoftware.smackx.pubsub.PubSubManager;
046import org.jivesoftware.smackx.pubsub.filter.EventExtensionFilter;
047
048import org.jxmpp.jid.BareJid;
049import org.jxmpp.jid.EntityBareJid;
050
051/**
052 *
053 * Manages Personal Event Publishing (XEP-163). A PEPManager provides a high level access to
054 * PubSub personal events. It also provides an easy way
055 * to hook up custom logic when events are received from another XMPP client through PEPListeners.
056 *
057 * Use example:
058 *
059 * <pre>
060 *   PEPManager pepManager = new PEPManager(smackConnection);
061 *   pepManager.addPEPListener(new PEPListener() {
062 *       public void eventReceived(EntityBareJid from, EventElement event, Message message) {
063 *           LOGGER.debug("Event received: " + event);
064 *       }
065 *   });
066 * </pre>
067 *
068 * @author Jeff Williams
069 * @author Florian Schmaus
070 */
071public final class PEPManager extends Manager {
072
073    private static final Map<XMPPConnection, PEPManager> INSTANCES = new WeakHashMap<>();
074
075    public static synchronized PEPManager getInstanceFor(XMPPConnection connection) {
076        PEPManager pepManager = INSTANCES.get(connection);
077        if (pepManager == null) {
078            pepManager = new PEPManager(connection);
079            INSTANCES.put(connection, pepManager);
080        }
081        return pepManager;
082    }
083
084    private static final StanzaFilter FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER = new AndFilter(
085            new FromJidTypeFilter(JidType.BareJid),
086            EventExtensionFilter.INSTANCE);
087
088    private final Set<PEPListener> pepListeners = new CopyOnWriteArraySet<>();
089
090    private final AsyncButOrdered<EntityBareJid> asyncButOrdered = new AsyncButOrdered<>();
091
092    /**
093     * Creates a new PEP exchange manager.
094     *
095     * @param connection an XMPPConnection which is used to send and receive messages.
096     */
097    private PEPManager(XMPPConnection connection) {
098        super(connection);
099        StanzaListener packetListener = new StanzaListener() {
100            @Override
101            public void processStanza(Stanza stanza) {
102                final Message message = (Message) stanza;
103                final EventElement event = EventElement.from(stanza);
104                assert (event != null);
105                final EntityBareJid from = message.getFrom().asEntityBareJidIfPossible();
106                assert (from != null);
107                asyncButOrdered.performAsyncButOrdered(from, new Runnable() {
108                    @Override
109                    public void run() {
110                        for (PEPListener listener : pepListeners) {
111                            listener.eventReceived(from, event, message);
112                        }
113                    }
114                });
115            }
116        };
117        // TODO Add filter to check if from supports PubSub as per xep163 2 2.4
118        connection.addSyncStanzaListener(packetListener, FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER);
119    }
120
121    /**
122     * Adds a listener to PEPs. The listener will be fired anytime PEP events
123     * are received from remote XMPP clients.
124     *
125     * @param pepListener a roster exchange listener.
126     * @return true if pepListener was added.
127     */
128    public boolean addPEPListener(PEPListener pepListener) {
129        return pepListeners.add(pepListener);
130    }
131
132    /**
133     * Removes a listener from PEP events.
134     *
135     * @param pepListener a roster exchange listener.
136     * @return true, if pepListener was removed.
137     */
138    public boolean removePEPListener(PEPListener pepListener) {
139        return pepListeners.remove(pepListener);
140    }
141
142    /**
143     * Publish an event.
144     *
145     * @param item the item to publish.
146     * @param node the node to publish on.
147     * @throws NotConnectedException
148     * @throws InterruptedException
149     * @throws XMPPErrorException
150     * @throws NoResponseException
151     * @throws NotAPubSubNodeException
152     */
153    public void publish(Item item, String node) throws NotConnectedException, InterruptedException,
154                    NoResponseException, XMPPErrorException, NotAPubSubNodeException {
155        XMPPConnection connection = connection();
156        PubSubManager pubSubManager = PubSubManager.getInstance(connection, connection.getUser().asEntityBareJid());
157        LeafNode pubSubNode = pubSubManager.getNode(node);
158        pubSubNode.publish(item);
159    }
160
161    /**
162     * XEP-163 5.
163     */
164    private static final PubSubFeature[] REQUIRED_FEATURES = new PubSubFeature[] {
165        // @formatter:off
166        PubSubFeature.auto_create,
167        PubSubFeature.auto_subscribe,
168        PubSubFeature.filtered_notifications,
169        // @formatter:on
170    };
171
172    public boolean isSupported() throws NoResponseException, XMPPErrorException,
173                    NotConnectedException, InterruptedException {
174        XMPPConnection connection = connection();
175        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
176        BareJid localBareJid = connection.getUser().asBareJid();
177        return serviceDiscoveryManager.supportsFeatures(localBareJid, REQUIRED_FEATURES);
178    }
179}