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 153 @SuppressWarnings("ObjectToString") 154 @Override 155 public IQ handleIQRequest(IQ iqRequest) { 156 if (!isRegistry(iqRequest.getFrom())) { 157 LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest); 158 return null; 159 } 160 161 IoTDisowned iotDisowned = (IoTDisowned) iqRequest; 162 Jid from = iqRequest.getFrom(); 163 164 NodeInfo nodeInfo = iotDisowned.getNodeInfo(); 165 ThingState state = getStateFor(nodeInfo); 166 if (!from.equals(state.getRegistry())) { 167 LOGGER.severe("Received <disowned/> for " + nodeInfo + " from " + from 168 + " but this is not the registry " + state.getRegistry() + " of the thing."); 169 return null; 170 } 171 172 if (state.isOwned()) { 173 state.setUnowned(); 174 } else { 175 LOGGER.fine("Received <disowned/> for " + nodeInfo + " but thing was not owned."); 176 } 177 178 return IQ.createResultIQ(iqRequest); 179 } 180 }); 181 182 // XEP-0347 § 3.9 (ex28-29): <removed/> 183 connection.registerIQRequestHandler(new AbstractIqRequestHandler(IoTRemoved.ELEMENT, IoTRemoved.NAMESPACE, IQ.Type.set, Mode.async) { 184 @Override 185 public IQ handleIQRequest(IQ iqRequest) { 186 if (!isRegistry(iqRequest.getFrom())) { 187 LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest); 188 return null; 189 } 190 191 IoTRemoved iotRemoved = (IoTRemoved) iqRequest; 192 193 ThingState state = getStateFor(iotRemoved.getNodeInfo()); 194 state.setRemoved(); 195 196 // Unfriend registry. "It does this, so the Thing can remove the friendship and stop any 197 // meta data updates to the Registry." 198 try { 199 IoTProvisioningManager.getInstanceFor(connection()).unfriend(iotRemoved.getFrom()); 200 } 201 catch (NotConnectedException | InterruptedException e) { 202 LOGGER.log(Level.SEVERE, "Could not unfriend registry after <removed/>", e); 203 } 204 205 return IQ.createResultIQ(iqRequest); 206 } 207 }); 208 } 209 210 /** 211 * Try to find an XMPP IoT registry. 212 * 213 * @return the JID of a Thing Registry if one could be found, <code>null</code> otherwise. 214 * @throws InterruptedException if the calling thread was interrupted. 215 * @throws NotConnectedException if the XMPP connection is not connected. 216 * @throws XMPPErrorException if there was an XMPP error returned. 217 * @throws NoResponseException if there was no response from the remote entity. 218 * @see <a href="http://xmpp.org/extensions/xep-0347.html#findingregistry">XEP-0347 § 3.5 Finding Thing Registry</a> 219 */ 220 public Jid findRegistry() 221 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 222 if (preconfiguredRegistry != null) { 223 return preconfiguredRegistry; 224 } 225 226 final XMPPConnection connection = connection(); 227 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); 228 List<DiscoverInfo> discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_DISCOVERY_NAMESPACE, true, true); 229 if (!discoverInfos.isEmpty()) { 230 return discoverInfos.get(0).getFrom(); 231 } 232 233 return null; 234 } 235 236 // Thing Registration - XEP-0347 § 3.6 - 3.8 237 238 public ThingState registerThing(Thing thing) 239 throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException { 240 Jid registry = findRegistry(); 241 return registerThing(registry, thing); 242 } 243 244 public ThingState registerThing(Jid registry, Thing thing) 245 throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException { 246 final XMPPConnection connection = connection(); 247 IoTRegister iotRegister = new IoTRegister(thing.getMetaTags(), thing.getNodeInfo(), thing.isSelfOwened()); 248 iotRegister.setTo(registry); 249 IQ result = connection.createStanzaCollectorAndSend(iotRegister).nextResultOrThrow(); 250 if (result instanceof IoTClaimed) { 251 IoTClaimed iotClaimedResult = (IoTClaimed) result; 252 throw new IoTClaimedException(iotClaimedResult); 253 } 254 255 ThingState state = getStateFor(thing.getNodeInfo()); 256 state.setRegistry(registry.asBareJid()); 257 258 interactWithRegistry(registry); 259 260 IoTDataManager.getInstanceFor(connection).installThing(thing); 261 IoTControlManager.getInstanceFor(connection).installThing(thing); 262 263 return state; 264 } 265 266 // Thing Claiming - XEP-0347 § 3.9 267 268 public IoTClaimed claimThing(Collection<Tag> metaTags) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 269 return claimThing(metaTags, true); 270 } 271 272 public IoTClaimed claimThing(Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 273 Jid registry = findRegistry(); 274 return claimThing(registry, metaTags, publicThing); 275 } 276 277 /** 278 * Claim a thing by providing a collection of meta tags. If the claim was successful, then a {@link IoTClaimed} 279 * instance will be returned, which contains the XMPP address of the thing. Use {@link IoTClaimed#getJid()} to 280 * retrieve this address. 281 * 282 * @param registry the registry use to claim the thing. 283 * @param metaTags a collection of meta tags used to identify the thing. 284 * @param publicThing if this is a public thing. 285 * @return a {@link IoTClaimed} if successful. 286 * @throws NoResponseException if there was no response from the remote entity. 287 * @throws XMPPErrorException if there was an XMPP error returned. 288 * @throws NotConnectedException if the XMPP connection is not connected. 289 * @throws InterruptedException if the calling thread was interrupted. 290 */ 291 public IoTClaimed claimThing(Jid registry, Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 292 interactWithRegistry(registry); 293 294 IoTMine iotMine = new IoTMine(metaTags, publicThing); 295 iotMine.setTo(registry); 296 IoTClaimed iotClaimed = connection().createStanzaCollectorAndSend(iotMine).nextResultOrThrow(); 297 298 // The 'jid' attribute of the <claimed/> response now represents the XMPP address of the thing we just successfully claimed. 299 Jid thing = iotClaimed.getJid(); 300 301 IoTProvisioningManager.getInstanceFor(connection()).sendFriendshipRequest(thing.asBareJid()); 302 303 return iotClaimed; 304 } 305 306 // Thing Removal - XEP-0347 § 3.10 307 308 public void removeThing(BareJid thing) 309 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 310 removeThing(thing, NodeInfo.EMPTY); 311 } 312 313 public void removeThing(BareJid thing, NodeInfo nodeInfo) 314 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 315 Jid registry = findRegistry(); 316 removeThing(registry, thing, nodeInfo); 317 } 318 319 public void removeThing(Jid registry, BareJid thing, NodeInfo nodeInfo) 320 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 321 interactWithRegistry(registry); 322 323 IoTRemove iotRemove = new IoTRemove(thing, nodeInfo); 324 iotRemove.setTo(registry); 325 connection().createStanzaCollectorAndSend(iotRemove).nextResultOrThrow(); 326 327 // We no not update the ThingState here, as this is done in the <removed/> IQ handler above.; 328 } 329 330 // Thing Unregistering - XEP-0347 § 3.16 331 332 public void unregister() 333 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 334 unregister(NodeInfo.EMPTY); 335 } 336 337 public void unregister(NodeInfo nodeInfo) 338 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 339 Jid registry = findRegistry(); 340 unregister(registry, nodeInfo); 341 } 342 343 public void unregister(Jid registry, NodeInfo nodeInfo) 344 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 345 interactWithRegistry(registry); 346 347 IoTUnregister iotUnregister = new IoTUnregister(nodeInfo); 348 iotUnregister.setTo(registry); 349 connection().createStanzaCollectorAndSend(iotUnregister).nextResultOrThrow(); 350 351 ThingState state = getStateFor(nodeInfo); 352 state.setUnregistered(); 353 354 final XMPPConnection connection = connection(); 355 IoTDataManager.getInstanceFor(connection).uninstallThing(nodeInfo); 356 IoTControlManager.getInstanceFor(connection).uninstallThing(nodeInfo); 357 } 358 359 // Thing Disowning - XEP-0347 § 3.17 360 361 public void disownThing(Jid thing) 362 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 363 disownThing(thing, NodeInfo.EMPTY); 364 } 365 366 public void disownThing(Jid thing, NodeInfo nodeInfo) 367 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 368 Jid registry = findRegistry(); 369 disownThing(registry, thing, nodeInfo); 370 } 371 372 public void disownThing(Jid registry, Jid thing, NodeInfo nodeInfo) 373 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 374 interactWithRegistry(registry); 375 376 IoTDisown iotDisown = new IoTDisown(thing, nodeInfo); 377 iotDisown.setTo(registry); 378 connection().createStanzaCollectorAndSend(iotDisown).nextResultOrThrow(); 379 } 380 381 // Registry utility methods 382 383 public boolean isRegistry(BareJid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 384 Objects.requireNonNull(jid, "JID argument must not be null"); 385 // At some point 'usedRegistries' will also contain the registry returned by findRegistry(), but since this is 386 // not the case from the beginning, we perform findRegistry().equals(jid) too. 387 Jid registry = findRegistry(); 388 if (jid.equals(registry)) { 389 return true; 390 } 391 if (usedRegistries.contains(jid)) { 392 return true; 393 } 394 return false; 395 } 396 397 public boolean isRegistry(Jid jid) { 398 try { 399 return isRegistry(jid.asBareJid()); 400 } 401 catch (NoResponseException | XMPPErrorException | NotConnectedException 402 | InterruptedException e) { 403 LOGGER.log(Level.WARNING, "Could not determine if " + jid + " is a registry", e); 404 return false; 405 } 406 } 407 408 private void interactWithRegistry(Jid registry) throws NotConnectedException, InterruptedException { 409 boolean isNew = usedRegistries.add(registry); 410 if (!isNew) { 411 return; 412 } 413 IoTProvisioningManager iotProvisioningManager = IoTProvisioningManager.getInstanceFor(connection()); 414 iotProvisioningManager.sendFriendshipRequestIfRequired(registry.asBareJid()); 415 } 416 417 public ThingState getStateFor(Thing thing) { 418 return things.get(thing.getNodeInfo()); 419 } 420 421 private ThingState getStateFor(NodeInfo nodeInfo) { 422 ThingState state = things.get(nodeInfo); 423 if (state == null) { 424 state = new ThingState(nodeInfo); 425 things.put(nodeInfo, state); 426 } 427 return state; 428 } 429 430}