001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2015-2024 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.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.WeakHashMap;
025import java.util.concurrent.CopyOnWriteArraySet;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.AsyncButOrdered;
029import org.jivesoftware.smack.Manager;
030import org.jivesoftware.smack.SmackException.NoResponseException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.StanzaListener;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.XMPPException.XMPPErrorException;
035import org.jivesoftware.smack.filter.AndFilter;
036import org.jivesoftware.smack.filter.MessageTypeFilter;
037import org.jivesoftware.smack.filter.StanzaFilter;
038import org.jivesoftware.smack.filter.jidtype.FromJidTypeFilter;
039import org.jivesoftware.smack.packet.ExtensionElement;
040import org.jivesoftware.smack.packet.Message;
041import org.jivesoftware.smack.packet.NamedElement;
042import org.jivesoftware.smack.packet.Stanza;
043import org.jivesoftware.smack.util.CollectionUtil;
044import org.jivesoftware.smack.util.MultiMap;
045
046import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
047import org.jivesoftware.smackx.pubsub.EventElement;
048import org.jivesoftware.smackx.pubsub.Item;
049import org.jivesoftware.smackx.pubsub.ItemsExtension;
050import org.jivesoftware.smackx.pubsub.LeafNode;
051import org.jivesoftware.smackx.pubsub.PayloadItem;
052import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
053import org.jivesoftware.smackx.pubsub.PubSubFeature;
054import org.jivesoftware.smackx.pubsub.PubSubManager;
055import org.jivesoftware.smackx.pubsub.filter.EventItemsExtensionFilter;
056
057import org.jxmpp.jid.BareJid;
058import org.jxmpp.jid.EntityBareJid;
059
060/**
061 *
062 * Manages Personal Event Publishing (XEP-163). A PEPManager provides a high level access to
063 * PubSub personal events. It also provides an easy way
064 * to hook up custom logic when events are received from another XMPP client through PEPListeners.
065 *
066 * Use example:
067 *
068 * <pre>
069 *   PepManager pepManager = PepManager.getInstanceFor(smackConnection);
070 *   pepManager.addPepListener(new PepListener() {
071 *       public void eventReceived(EntityBareJid from, EventElement event, Message message) {
072 *           LOGGER.debug("Event received: " + event);
073 *       }
074 *   });
075 * </pre>
076 *
077 * @author Jeff Williams
078 * @author Florian Schmaus
079 */
080public final class PepManager extends Manager {
081
082    private static final Logger LOGGER = Logger.getLogger(PepManager.class.getName());
083
084    private static final Map<XMPPConnection, PepManager> INSTANCES = new WeakHashMap<>();
085
086    public static synchronized PepManager getInstanceFor(XMPPConnection connection) {
087        PepManager pepManager = INSTANCES.get(connection);
088        if (pepManager == null) {
089            pepManager = new PepManager(connection);
090            INSTANCES.put(connection, pepManager);
091        }
092        return pepManager;
093    }
094
095    // TODO: Ideally PepManager would re-use PubSubManager for this. But the functionality in PubSubManager does not yet
096    // exist.
097    private static final StanzaFilter PEP_EVENTS_FILTER = new AndFilter(
098            MessageTypeFilter.NORMAL_OR_HEADLINE,
099            FromJidTypeFilter.ENTITY_BARE_JID,
100            EventItemsExtensionFilter.INSTANCE);
101
102    private final Set<PepListener> pepListeners = new CopyOnWriteArraySet<>();
103
104    private final AsyncButOrdered<EntityBareJid> asyncButOrdered = new AsyncButOrdered<>();
105
106    private final ServiceDiscoveryManager serviceDiscoveryManager;
107
108    private final PubSubManager pepPubSubManager;
109
110    private final MultiMap<String, PepEventListenerCoupling<? extends ExtensionElement>> pepEventListeners = new MultiMap<>();
111
112    private final Map<PepEventListener<?>, PepEventListenerCoupling<?>> listenerToCouplingMap = new HashMap<>();
113
114    /**
115     * Creates a new PEP exchange manager.
116     *
117     * @param connection an XMPPConnection which is used to send and receive messages.
118     */
119    private PepManager(XMPPConnection connection) {
120        super(connection);
121
122        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
123        pepPubSubManager = PubSubManager.getInstanceFor(connection, null);
124
125        StanzaListener packetListener = new StanzaListener() {
126            @Override
127            public void processStanza(Stanza stanza) {
128                final Message message = (Message) stanza;
129                final EventElement event = EventElement.from(stanza);
130                assert event != null;
131                final EntityBareJid from = message.getFrom().asEntityBareJidIfPossible();
132                assert from != null;
133
134                asyncButOrdered.performAsyncButOrdered(from, new Runnable() {
135                    @Override
136                    public void run() {
137                        ItemsExtension itemsExtension = (ItemsExtension) event.getEvent();
138                        String node = itemsExtension.getNode();
139
140                        for (PepListener listener : pepListeners) {
141                            listener.eventReceived(from, event, message);
142                        }
143
144                        List<PepEventListenerCoupling<? extends ExtensionElement>> nodeListeners;
145                        synchronized (pepEventListeners) {
146                            nodeListeners = pepEventListeners.getAll(node);
147                            if (nodeListeners.isEmpty()) {
148                                return;
149                            }
150
151                            // Make a copy of the list. Note that it is important to do this within the synchronized
152                            // block.
153                            nodeListeners = CollectionUtil.newListWith(nodeListeners);
154                        }
155
156                        for (PepEventListenerCoupling<? extends ExtensionElement> listener : nodeListeners) {
157                            // TODO: Can there be more than one item?
158                            List<? extends NamedElement> items = itemsExtension.getItems();
159                            for (NamedElement namedElementItem : items) {
160                                Item item = (Item) namedElementItem;
161                                String id = item.getId();
162                                @SuppressWarnings("unchecked")
163                                PayloadItem<ExtensionElement> payloadItem = (PayloadItem<ExtensionElement>) item;
164                                ExtensionElement payload = payloadItem.getPayload();
165
166                                listener.invoke(from, payload, id, message);
167                            }
168                        }
169                    }
170                });
171            }
172        };
173        // TODO Add filter to check if from supports PubSub as per xep163 2 2.4
174        connection.addSyncStanzaListener(packetListener, PEP_EVENTS_FILTER);
175    }
176
177    private static final class PepEventListenerCoupling<E extends ExtensionElement> {
178        private final String node;
179        private final Class<E> extensionElementType;
180        private final PepEventListener<E> pepEventListener;
181
182        private PepEventListenerCoupling(String node, Class<E> extensionElementType,
183                        PepEventListener<E> pepEventListener) {
184            this.node = node;
185            this.extensionElementType = extensionElementType;
186            this.pepEventListener = pepEventListener;
187        }
188
189        private void invoke(EntityBareJid from, ExtensionElement payload, String id, Message carrierMessage) {
190            if (!extensionElementType.isInstance(payload)) {
191                LOGGER.warning("Ignoring " + payload + " from " + carrierMessage + " as it is not of type "
192                                + extensionElementType);
193                return;
194            }
195
196            E extensionElementPayload = extensionElementType.cast(payload);
197            pepEventListener.onPepEvent(from, extensionElementPayload, id, carrierMessage);
198        }
199    }
200
201    public <E extends ExtensionElement> boolean addPepEventListener(String node, Class<E> extensionElementType,
202                    PepEventListener<E> pepEventListener) {
203        PepEventListenerCoupling<E> pepEventListenerCoupling = new PepEventListenerCoupling<>(node,
204                        extensionElementType, pepEventListener);
205
206        synchronized (pepEventListeners) {
207            if (listenerToCouplingMap.containsKey(pepEventListener)) {
208                return false;
209            }
210            listenerToCouplingMap.put(pepEventListener, pepEventListenerCoupling);
211            /*
212             * TODO: Replace the above with the below using putIfAbsent() if Smack's minimum required Android SDK level
213             * is 24 or higher. PepEventListenerCoupling<?> currentPepEventListenerCoupling =
214             * listenerToCouplingMap.putIfAbsent(pepEventListener, pepEventListenerCoupling); if
215             * (currentPepEventListenerCoupling != null) { return false; }
216             */
217
218            boolean listenerForNodeExisted = pepEventListeners.put(node, pepEventListenerCoupling);
219            if (!listenerForNodeExisted) {
220                serviceDiscoveryManager.addFeature(node + PubSubManager.PLUS_NOTIFY);
221            }
222        }
223        return true;
224    }
225
226    public boolean removePepEventListener(PepEventListener<?> pepEventListener) {
227        synchronized (pepEventListeners) {
228            PepEventListenerCoupling<?> pepEventListenerCoupling = listenerToCouplingMap.remove(pepEventListener);
229            if (pepEventListenerCoupling == null) {
230                return false;
231            }
232
233            String node = pepEventListenerCoupling.node;
234
235            boolean mappingExisted = pepEventListeners.removeOne(node, pepEventListenerCoupling);
236            assert mappingExisted;
237
238            if (!pepEventListeners.containsKey(pepEventListenerCoupling.node)) {
239                // This was the last listener for the node. Remove the +notify feature.
240                serviceDiscoveryManager.removeFeature(node + PubSubManager.PLUS_NOTIFY);
241            }
242        }
243
244        return true;
245    }
246
247    public PubSubManager getPepPubSubManager() {
248        return pepPubSubManager;
249    }
250
251    /**
252     * Publish an event.
253     *
254     * @param nodeId the ID of the node to publish on.
255     * @param item the item to publish.
256     * @return the leaf node the item was published on.
257     * @throws NotConnectedException if the XMPP connection is not connected.
258     * @throws InterruptedException if the calling thread was interrupted.
259     * @throws XMPPErrorException if there was an XMPP error returned.
260     * @throws NoResponseException if there was no response from the remote entity.
261     * @throws NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
262     */
263    public LeafNode publish(String nodeId, Item item) throws NotConnectedException, InterruptedException,
264                    NoResponseException, XMPPErrorException, NotALeafNodeException {
265        // PEP nodes are auto created if not existent. Hence, use PubSubManager.tryToPublishAndPossibleAutoCreate() here.
266        return pepPubSubManager.tryToPublishAndPossibleAutoCreate(nodeId, item);
267    }
268
269    /**
270     * XEP-163 5.
271     */
272    private static final PubSubFeature[] REQUIRED_FEATURES = new PubSubFeature[] {
273        // @formatter:off
274        PubSubFeature.auto_create,
275        PubSubFeature.auto_subscribe,
276        PubSubFeature.filtered_notifications,
277        // @formatter:on
278    };
279
280    public boolean isSupported()
281                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
282        XMPPConnection connection = connection();
283        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
284        BareJid localBareJid = connection.getUser().asBareJid();
285        return serviceDiscoveryManager.supportsFeatures(localBareJid, REQUIRED_FEATURES);
286    }
287}