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