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}