001/** 002 * 003 * Copyright 2016 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 */ 017package org.jivesoftware.smackx.iot.discovery; 018 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.WeakHashMap; 026import java.util.logging.Level; 027import java.util.logging.Logger; 028 029import org.jivesoftware.smack.ConnectionCreationListener; 030import org.jivesoftware.smack.Manager; 031import org.jivesoftware.smack.SmackException.NoResponseException; 032import org.jivesoftware.smack.SmackException.NotConnectedException; 033import org.jivesoftware.smack.XMPPConnection; 034import org.jivesoftware.smack.XMPPConnectionRegistry; 035import org.jivesoftware.smack.XMPPException.XMPPErrorException; 036import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 037import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 038import org.jivesoftware.smack.packet.IQ; 039import org.jivesoftware.smack.util.Objects; 040import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 041import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 042import org.jivesoftware.smackx.iot.IoTManager; 043import org.jivesoftware.smackx.iot.Thing; 044import org.jivesoftware.smackx.iot.control.IoTControlManager; 045import org.jivesoftware.smackx.iot.data.IoTDataManager; 046import org.jivesoftware.smackx.iot.discovery.element.Constants; 047import org.jivesoftware.smackx.iot.discovery.element.IoTClaimed; 048import org.jivesoftware.smackx.iot.discovery.element.IoTDisown; 049import org.jivesoftware.smackx.iot.discovery.element.IoTDisowned; 050import org.jivesoftware.smackx.iot.discovery.element.IoTMine; 051import org.jivesoftware.smackx.iot.discovery.element.IoTRegister; 052import org.jivesoftware.smackx.iot.discovery.element.IoTRemove; 053import org.jivesoftware.smackx.iot.discovery.element.IoTRemoved; 054import org.jivesoftware.smackx.iot.discovery.element.IoTUnregister; 055import org.jivesoftware.smackx.iot.discovery.element.Tag; 056import org.jivesoftware.smackx.iot.element.NodeInfo; 057import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager; 058import org.jxmpp.jid.BareJid; 059import org.jxmpp.jid.Jid; 060 061/** 062 * A manager for XEP-0347: Internet of Things - Discovery. Used to register and discover things. 063 * 064 * @author Florian Schmaus {@literal <flo@geekplace.eu>} 065 * @see <a href="http://xmpp.org/extensions/xep-0347.html">XEP-0347: Internet of Things - Discovery</a> 066 * 067 */ 068public final class IoTDiscoveryManager extends Manager { 069 070 private static final Logger LOGGER = Logger.getLogger(IoTDiscoveryManager.class.getName()); 071 072 private static final Map<XMPPConnection, IoTDiscoveryManager> INSTANCES = new WeakHashMap<>(); 073 074 // Ensure a IoTProvisioningManager exists for every connection. 075 static { 076 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 077 @Override 078 public void connectionCreated(XMPPConnection connection) { 079 if (!IoTManager.isAutoEnableActive()) return; 080 getInstanceFor(connection); 081 } 082 }); 083 } 084 085 /** 086 * Get the manger instance responsible for the given connection. 087 * 088 * @param connection the XMPP connection. 089 * @return a manager instance. 090 */ 091 public static synchronized IoTDiscoveryManager getInstanceFor(XMPPConnection connection) { 092 IoTDiscoveryManager manager = INSTANCES.get(connection); 093 if (manager == null) { 094 manager = new IoTDiscoveryManager(connection); 095 INSTANCES.put(connection, manager); 096 } 097 return manager; 098 } 099 100 private Jid preconfiguredRegistry; 101 102 /** 103 * A set of all registries we have interacted so far. {@link #isRegistry(BareJid)} uses this to 104 * determine if the jid is a registry. Note that we currently do not record which thing 105 * interacted with which registry. This allows any registry we have interacted so far with, to 106 * send registry control stanzas about any other thing, and we would process them. 107 */ 108 private final Set<Jid> usedRegistries = new HashSet<>(); 109 110 /** 111 * Internal state of the things. Uses <code>null</code> for the single thing without node info attached. 112 */ 113 private final Map<NodeInfo, ThingState> things = new HashMap<>(); 114 115 private IoTDiscoveryManager(XMPPConnection connection) { 116 super(connection); 117 118 connection.registerIQRequestHandler( 119 new AbstractIqRequestHandler(IoTClaimed.ELEMENT, IoTClaimed.NAMESPACE, IQ.Type.set, Mode.sync) { 120 @Override 121 public IQ handleIQRequest(IQ iqRequest) { 122 if (!isRegistry(iqRequest.getFrom())) { 123 LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest); 124 return null; 125 } 126 127 IoTClaimed iotClaimed = (IoTClaimed) iqRequest; 128 Jid owner = iotClaimed.getJid(); 129 NodeInfo nodeInfo = iotClaimed.getNodeInfo(); 130 // Update the state. 131 ThingState state = getStateFor(nodeInfo); 132 state.setOwner(owner.asBareJid()); 133 LOGGER.info("Our thing got claimed by " + owner + ". " + iotClaimed); 134 135 IoTProvisioningManager iotProvisioningManager = IoTProvisioningManager.getInstanceFor( 136 connection()); 137 try { 138 iotProvisioningManager.sendFriendshipRequest(owner.asBareJid()); 139 } 140 catch (NotConnectedException | InterruptedException e) { 141 LOGGER.log(Level.WARNING, "Could not friendship owner", e); 142 } 143 144 return IQ.createResultIQ(iqRequest); 145 } 146 }); 147 148 connection.registerIQRequestHandler(new AbstractIqRequestHandler(IoTDisowned.ELEMENT, IoTDisowned.NAMESPACE, 149 IQ.Type.set, Mode.sync) { 150 @Override 151 public IQ handleIQRequest(IQ iqRequest) { 152 if (!isRegistry(iqRequest.getFrom())) { 153 LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest); 154 return null; 155 } 156 157 IoTDisowned iotDisowned = (IoTDisowned) iqRequest; 158 Jid from = iqRequest.getFrom(); 159 160 NodeInfo nodeInfo = iotDisowned.getNodeInfo(); 161 ThingState state = getStateFor(nodeInfo); 162 if (!from.equals(state.getRegistry())) { 163 LOGGER.severe("Received <disowned/> for " + nodeInfo + " from " + from 164 + " but this is not the registry " + state.getRegistry() + " of the thing."); 165 return null; 166 } 167 168 if (state.isOwned()) { 169 state.setUnowned(); 170 } else { 171 LOGGER.fine("Received <disowned/> for " + nodeInfo + " but thing was not owned."); 172 } 173 174 return IQ.createResultIQ(iqRequest); 175 } 176 }); 177 178 // XEP-0347 § 3.9 (ex28-29): <removed/> 179 connection.registerIQRequestHandler(new AbstractIqRequestHandler(IoTRemoved.ELEMENT, IoTRemoved.NAMESPACE, IQ.Type.set, Mode.async) { 180 @Override 181 public IQ handleIQRequest(IQ iqRequest) { 182 if (!isRegistry(iqRequest.getFrom())) { 183 LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest); 184 return null; 185 } 186 187 IoTRemoved iotRemoved = (IoTRemoved) iqRequest; 188 189 ThingState state = getStateFor(iotRemoved.getNodeInfo()); 190 state.setRemoved(); 191 192 // Unfriend registry. "It does this, so the Thing can remove the friendship and stop any 193 // meta data updates to the Registry." 194 try { 195 IoTProvisioningManager.getInstanceFor(connection()).unfriend(iotRemoved.getFrom()); 196 } 197 catch (NotConnectedException | InterruptedException e) { 198 LOGGER.log(Level.SEVERE, "Could not unfriend registry after <removed/>", e); 199 } 200 201 return IQ.createResultIQ(iqRequest); 202 } 203 }); 204 } 205 206 /** 207 * Try to find an XMPP IoT registry. 208 * 209 * @return the JID of a Thing Registry if one could be found, <code>null</code> otherwise. 210 * @throws InterruptedException 211 * @throws NotConnectedException 212 * @throws XMPPErrorException 213 * @throws NoResponseException 214 * @see <a href="http://xmpp.org/extensions/xep-0347.html#findingregistry">XEP-0347 § 3.5 Finding Thing Registry</a> 215 */ 216 public Jid findRegistry() 217 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 218 if (preconfiguredRegistry != null) { 219 return preconfiguredRegistry; 220 } 221 222 final XMPPConnection connection = connection(); 223 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); 224 List<DiscoverInfo> discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_DISCOVERY_NAMESPACE, true, true); 225 if (!discoverInfos.isEmpty()) { 226 return discoverInfos.get(0).getFrom(); 227 } 228 229 return null; 230 } 231 232 // Thing Registration - XEP-0347 § 3.6 - 3.8 233 234 public ThingState registerThing(Thing thing) 235 throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException { 236 Jid registry = findRegistry(); 237 return registerThing(registry, thing); 238 } 239 240 public ThingState registerThing(Jid registry, Thing thing) 241 throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException { 242 final XMPPConnection connection = connection(); 243 IoTRegister iotRegister = new IoTRegister(thing.getMetaTags(), thing.getNodeInfo(), thing.isSelfOwened()); 244 iotRegister.setTo(registry); 245 IQ result = connection.createStanzaCollectorAndSend(iotRegister).nextResultOrThrow(); 246 if (result instanceof IoTClaimed) { 247 IoTClaimed iotClaimedResult = (IoTClaimed) result; 248 throw new IoTClaimedException(iotClaimedResult); 249 } 250 251 ThingState state = getStateFor(thing.getNodeInfo()); 252 state.setRegistry(registry.asBareJid()); 253 254 interactWithRegistry(registry); 255 256 IoTDataManager.getInstanceFor(connection).installThing(thing); 257 IoTControlManager.getInstanceFor(connection).installThing(thing); 258 259 return state; 260 } 261 262 // Thing Claiming - XEP-0347 § 3.9 263 264 public IoTClaimed claimThing(Collection<Tag> metaTags) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 265 return claimThing(metaTags, true); 266 } 267 268 public IoTClaimed claimThing(Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 269 Jid registry = findRegistry(); 270 return claimThing(registry, metaTags, publicThing); 271 } 272 273 /** 274 * Claim a thing by providing a collection of meta tags. If the claim was successful, then a {@link IoTClaimed} 275 * instance will be returned, which contains the XMPP address of the thing. Use {@link IoTClaimed#getJid()} to 276 * retrieve this address. 277 * 278 * @param registry the registry use to claim the thing. 279 * @param metaTags a collection of meta tags used to identify the thing. 280 * @param publicThing if this is a public thing. 281 * @return a {@link IoTClaimed} if successful. 282 * @throws NoResponseException 283 * @throws XMPPErrorException 284 * @throws NotConnectedException 285 * @throws InterruptedException 286 */ 287 public IoTClaimed claimThing(Jid registry, Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 288 interactWithRegistry(registry); 289 290 IoTMine iotMine = new IoTMine(metaTags, publicThing); 291 iotMine.setTo(registry); 292 IoTClaimed iotClaimed = connection().createStanzaCollectorAndSend(iotMine).nextResultOrThrow(); 293 294 // The 'jid' attribute of the <claimed/> response now represents the XMPP address of the thing we just successfully claimed. 295 Jid thing = iotClaimed.getJid(); 296 297 IoTProvisioningManager.getInstanceFor(connection()).sendFriendshipRequest(thing.asBareJid()); 298 299 return iotClaimed; 300 } 301 302 // Thing Removal - XEP-0347 § 3.10 303 304 public void removeThing(BareJid thing) 305 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 306 removeThing(thing, NodeInfo.EMPTY); 307 } 308 309 public void removeThing(BareJid thing, NodeInfo nodeInfo) 310 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 311 Jid registry = findRegistry(); 312 removeThing(registry, thing, nodeInfo); 313 } 314 315 public void removeThing(Jid registry, BareJid thing, NodeInfo nodeInfo) 316 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 317 interactWithRegistry(registry); 318 319 IoTRemove iotRemove = new IoTRemove(thing, nodeInfo); 320 iotRemove.setTo(registry); 321 connection().createStanzaCollectorAndSend(iotRemove).nextResultOrThrow(); 322 323 // We no not update the ThingState here, as this is done in the <removed/> IQ handler above.; 324 } 325 326 // Thing Unregistering - XEP-0347 § 3.16 327 328 public void unregister() 329 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 330 unregister(NodeInfo.EMPTY); 331 } 332 333 public void unregister(NodeInfo nodeInfo) 334 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 335 Jid registry = findRegistry(); 336 unregister(registry, nodeInfo); 337 } 338 339 public void unregister(Jid registry, NodeInfo nodeInfo) 340 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 341 interactWithRegistry(registry); 342 343 IoTUnregister iotUnregister = new IoTUnregister(nodeInfo); 344 iotUnregister.setTo(registry); 345 connection().createStanzaCollectorAndSend(iotUnregister).nextResultOrThrow(); 346 347 ThingState state = getStateFor(nodeInfo); 348 state.setUnregistered(); 349 350 final XMPPConnection connection = connection(); 351 IoTDataManager.getInstanceFor(connection).uninstallThing(nodeInfo); 352 IoTControlManager.getInstanceFor(connection).uninstallThing(nodeInfo); 353 } 354 355 // Thing Disowning - XEP-0347 § 3.17 356 357 public void disownThing(Jid thing) 358 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 359 disownThing(thing, NodeInfo.EMPTY); 360 } 361 362 public void disownThing(Jid thing, NodeInfo nodeInfo) 363 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 364 Jid registry = findRegistry(); 365 disownThing(registry, thing, nodeInfo); 366 } 367 368 public void disownThing(Jid registry, Jid thing, NodeInfo nodeInfo) 369 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 370 interactWithRegistry(registry); 371 372 IoTDisown iotDisown = new IoTDisown(thing, nodeInfo); 373 iotDisown.setTo(registry); 374 connection().createStanzaCollectorAndSend(iotDisown).nextResultOrThrow(); 375 } 376 377 // Registry utility methods 378 379 public boolean isRegistry(BareJid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 380 Objects.requireNonNull(jid, "JID argument must not be null"); 381 // At some point 'usedRegistries' will also contain the registry returned by findRegistry(), but since this is 382 // not the case from the beginning, we perform findRegistry().equals(jid) too. 383 Jid registry = findRegistry(); 384 if (jid.equals(registry)) { 385 return true; 386 } 387 if (usedRegistries.contains(jid)) { 388 return true; 389 } 390 return false; 391 } 392 393 public boolean isRegistry(Jid jid) { 394 try { 395 return isRegistry(jid.asBareJid()); 396 } 397 catch (NoResponseException | XMPPErrorException | NotConnectedException 398 | InterruptedException e) { 399 LOGGER.log(Level.WARNING, "Could not determine if " + jid + " is a registry", e); 400 return false; 401 } 402 } 403 404 private void interactWithRegistry(Jid registry) throws NotConnectedException, InterruptedException { 405 boolean isNew = usedRegistries.add(registry); 406 if (!isNew) { 407 return; 408 } 409 IoTProvisioningManager iotProvisioningManager = IoTProvisioningManager.getInstanceFor(connection()); 410 iotProvisioningManager.sendFriendshipRequestIfRequired(registry.asBareJid()); 411 } 412 413 public ThingState getStateFor(Thing thing) { 414 return things.get(thing.getNodeInfo()); 415 } 416 417 private ThingState getStateFor(NodeInfo nodeInfo) { 418 ThingState state = things.get(nodeInfo); 419 if (state == null) { 420 state = new ThingState(nodeInfo); 421 things.put(nodeInfo, state); 422 } 423 return state; 424 } 425 426}