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