IoTDiscoveryManager.java
/**
*
* Copyright 2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.iot.discovery;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.iot.IoTManager;
import org.jivesoftware.smackx.iot.Thing;
import org.jivesoftware.smackx.iot.control.IoTControlManager;
import org.jivesoftware.smackx.iot.data.IoTDataManager;
import org.jivesoftware.smackx.iot.discovery.element.Constants;
import org.jivesoftware.smackx.iot.discovery.element.IoTClaimed;
import org.jivesoftware.smackx.iot.discovery.element.IoTDisown;
import org.jivesoftware.smackx.iot.discovery.element.IoTDisowned;
import org.jivesoftware.smackx.iot.discovery.element.IoTMine;
import org.jivesoftware.smackx.iot.discovery.element.IoTRegister;
import org.jivesoftware.smackx.iot.discovery.element.IoTRemove;
import org.jivesoftware.smackx.iot.discovery.element.IoTRemoved;
import org.jivesoftware.smackx.iot.discovery.element.IoTUnregister;
import org.jivesoftware.smackx.iot.discovery.element.Tag;
import org.jivesoftware.smackx.iot.element.NodeInfo;
import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
/**
* A manager for XEP-0347: Internet of Things - Discovery. Used to register and discover things.
*
* @author Florian Schmaus {@literal <flo@geekplace.eu>}
* @see <a href="http://xmpp.org/extensions/xep-0347.html">XEP-0347: Internet of Things - Discovery</a>
*
*/
public final class IoTDiscoveryManager extends Manager {
private static final Logger LOGGER = Logger.getLogger(IoTDiscoveryManager.class.getName());
private static final Map<XMPPConnection, IoTDiscoveryManager> INSTANCES = new WeakHashMap<>();
// Ensure a IoTProvisioningManager exists for every connection.
static {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
@Override
public void connectionCreated(XMPPConnection connection) {
if (!IoTManager.isAutoEnableActive()) return;
getInstanceFor(connection);
}
});
}
/**
* Get the manger instance responsible for the given connection.
*
* @param connection the XMPP connection.
* @return a manager instance.
*/
public static synchronized IoTDiscoveryManager getInstanceFor(XMPPConnection connection) {
IoTDiscoveryManager manager = INSTANCES.get(connection);
if (manager == null) {
manager = new IoTDiscoveryManager(connection);
INSTANCES.put(connection, manager);
}
return manager;
}
private Jid preconfiguredRegistry;
/**
* A set of all registries we have interacted so far. {@link #isRegistry(BareJid)} uses this to
* determine if the jid is a registry. Note that we currently do not record which thing
* interacted with which registry. This allows any registry we have interacted so far with, to
* send registry control stanzas about any other thing, and we would process them.
*/
private final Set<Jid> usedRegistries = new HashSet<>();
/**
* Internal state of the things. Uses <code>null</code> for the single thing without node info attached.
*/
private final Map<NodeInfo, ThingState> things = new HashMap<>();
private IoTDiscoveryManager(XMPPConnection connection) {
super(connection);
connection.registerIQRequestHandler(
new AbstractIqRequestHandler(IoTClaimed.ELEMENT, IoTClaimed.NAMESPACE, IQ.Type.set, Mode.sync) {
@Override
public IQ handleIQRequest(IQ iqRequest) {
if (!isRegistry(iqRequest.getFrom())) {
LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest);
return null;
}
IoTClaimed iotClaimed = (IoTClaimed) iqRequest;
Jid owner = iotClaimed.getJid();
NodeInfo nodeInfo = iotClaimed.getNodeInfo();
// Update the state.
ThingState state = getStateFor(nodeInfo);
state.setOwner(owner.asBareJid());
LOGGER.info("Our thing got claimed by " + owner + ". " + iotClaimed);
IoTProvisioningManager iotProvisioningManager = IoTProvisioningManager.getInstanceFor(
connection());
try {
iotProvisioningManager.sendFriendshipRequest(owner.asBareJid());
}
catch (NotConnectedException | InterruptedException e) {
LOGGER.log(Level.WARNING, "Could not friendship owner", e);
}
return IQ.createResultIQ(iqRequest);
}
});
connection.registerIQRequestHandler(new AbstractIqRequestHandler(IoTDisowned.ELEMENT, IoTDisowned.NAMESPACE,
IQ.Type.set, Mode.sync) {
@SuppressWarnings("ObjectToString")
@Override
public IQ handleIQRequest(IQ iqRequest) {
if (!isRegistry(iqRequest.getFrom())) {
LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest);
return null;
}
IoTDisowned iotDisowned = (IoTDisowned) iqRequest;
Jid from = iqRequest.getFrom();
NodeInfo nodeInfo = iotDisowned.getNodeInfo();
ThingState state = getStateFor(nodeInfo);
if (!from.equals(state.getRegistry())) {
LOGGER.severe("Received <disowned/> for " + nodeInfo + " from " + from
+ " but this is not the registry " + state.getRegistry() + " of the thing.");
return null;
}
if (state.isOwned()) {
state.setUnowned();
} else {
LOGGER.fine("Received <disowned/> for " + nodeInfo + " but thing was not owned.");
}
return IQ.createResultIQ(iqRequest);
}
});
// XEP-0347 § 3.9 (ex28-29): <removed/>
connection.registerIQRequestHandler(new AbstractIqRequestHandler(IoTRemoved.ELEMENT, IoTRemoved.NAMESPACE, IQ.Type.set, Mode.async) {
@Override
public IQ handleIQRequest(IQ iqRequest) {
if (!isRegistry(iqRequest.getFrom())) {
LOGGER.log(Level.SEVERE, "Received control stanza from non-registry entity: " + iqRequest);
return null;
}
IoTRemoved iotRemoved = (IoTRemoved) iqRequest;
ThingState state = getStateFor(iotRemoved.getNodeInfo());
state.setRemoved();
// Unfriend registry. "It does this, so the Thing can remove the friendship and stop any
// meta data updates to the Registry."
try {
IoTProvisioningManager.getInstanceFor(connection()).unfriend(iotRemoved.getFrom());
}
catch (NotConnectedException | InterruptedException e) {
LOGGER.log(Level.SEVERE, "Could not unfriend registry after <removed/>", e);
}
return IQ.createResultIQ(iqRequest);
}
});
}
/**
* Try to find an XMPP IoT registry.
*
* @return the JID of a Thing Registry if one could be found, <code>null</code> otherwise.
* @throws InterruptedException if the calling thread was interrupted.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NoResponseException if there was no response from the remote entity.
* @see <a href="http://xmpp.org/extensions/xep-0347.html#findingregistry">XEP-0347 § 3.5 Finding Thing Registry</a>
*/
public Jid findRegistry()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
if (preconfiguredRegistry != null) {
return preconfiguredRegistry;
}
final XMPPConnection connection = connection();
ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
List<DiscoverInfo> discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_DISCOVERY_NAMESPACE, true, true);
if (!discoverInfos.isEmpty()) {
return discoverInfos.get(0).getFrom();
}
return null;
}
// Thing Registration - XEP-0347 § 3.6 - 3.8
public ThingState registerThing(Thing thing)
throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException {
Jid registry = findRegistry();
return registerThing(registry, thing);
}
public ThingState registerThing(Jid registry, Thing thing)
throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, IoTClaimedException {
final XMPPConnection connection = connection();
IoTRegister iotRegister = new IoTRegister(thing.getMetaTags(), thing.getNodeInfo(), thing.isSelfOwened());
iotRegister.setTo(registry);
IQ result = connection.createStanzaCollectorAndSend(iotRegister).nextResultOrThrow();
if (result instanceof IoTClaimed) {
IoTClaimed iotClaimedResult = (IoTClaimed) result;
throw new IoTClaimedException(iotClaimedResult);
}
ThingState state = getStateFor(thing.getNodeInfo());
state.setRegistry(registry.asBareJid());
interactWithRegistry(registry);
IoTDataManager.getInstanceFor(connection).installThing(thing);
IoTControlManager.getInstanceFor(connection).installThing(thing);
return state;
}
// Thing Claiming - XEP-0347 § 3.9
public IoTClaimed claimThing(Collection<Tag> metaTags) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return claimThing(metaTags, true);
}
public IoTClaimed claimThing(Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Jid registry = findRegistry();
return claimThing(registry, metaTags, publicThing);
}
/**
* Claim a thing by providing a collection of meta tags. If the claim was successful, then a {@link IoTClaimed}
* instance will be returned, which contains the XMPP address of the thing. Use {@link IoTClaimed#getJid()} to
* retrieve this address.
*
* @param registry the registry use to claim the thing.
* @param metaTags a collection of meta tags used to identify the thing.
* @param publicThing if this is a public thing.
* @return a {@link IoTClaimed} if successful.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public IoTClaimed claimThing(Jid registry, Collection<Tag> metaTags, boolean publicThing) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
interactWithRegistry(registry);
IoTMine iotMine = new IoTMine(metaTags, publicThing);
iotMine.setTo(registry);
IoTClaimed iotClaimed = connection().createStanzaCollectorAndSend(iotMine).nextResultOrThrow();
// The 'jid' attribute of the <claimed/> response now represents the XMPP address of the thing we just successfully claimed.
Jid thing = iotClaimed.getJid();
IoTProvisioningManager.getInstanceFor(connection()).sendFriendshipRequest(thing.asBareJid());
return iotClaimed;
}
// Thing Removal - XEP-0347 § 3.10
public void removeThing(BareJid thing)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
removeThing(thing, NodeInfo.EMPTY);
}
public void removeThing(BareJid thing, NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Jid registry = findRegistry();
removeThing(registry, thing, nodeInfo);
}
public void removeThing(Jid registry, BareJid thing, NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
interactWithRegistry(registry);
IoTRemove iotRemove = new IoTRemove(thing, nodeInfo);
iotRemove.setTo(registry);
connection().createStanzaCollectorAndSend(iotRemove).nextResultOrThrow();
// We no not update the ThingState here, as this is done in the <removed/> IQ handler above.;
}
// Thing Unregistering - XEP-0347 § 3.16
public void unregister()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
unregister(NodeInfo.EMPTY);
}
public void unregister(NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Jid registry = findRegistry();
unregister(registry, nodeInfo);
}
public void unregister(Jid registry, NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
interactWithRegistry(registry);
IoTUnregister iotUnregister = new IoTUnregister(nodeInfo);
iotUnregister.setTo(registry);
connection().createStanzaCollectorAndSend(iotUnregister).nextResultOrThrow();
ThingState state = getStateFor(nodeInfo);
state.setUnregistered();
final XMPPConnection connection = connection();
IoTDataManager.getInstanceFor(connection).uninstallThing(nodeInfo);
IoTControlManager.getInstanceFor(connection).uninstallThing(nodeInfo);
}
// Thing Disowning - XEP-0347 § 3.17
public void disownThing(Jid thing)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
disownThing(thing, NodeInfo.EMPTY);
}
public void disownThing(Jid thing, NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Jid registry = findRegistry();
disownThing(registry, thing, nodeInfo);
}
public void disownThing(Jid registry, Jid thing, NodeInfo nodeInfo)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
interactWithRegistry(registry);
IoTDisown iotDisown = new IoTDisown(thing, nodeInfo);
iotDisown.setTo(registry);
connection().createStanzaCollectorAndSend(iotDisown).nextResultOrThrow();
}
// Registry utility methods
public boolean isRegistry(BareJid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Objects.requireNonNull(jid, "JID argument must not be null");
// At some point 'usedRegistries' will also contain the registry returned by findRegistry(), but since this is
// not the case from the beginning, we perform findRegistry().equals(jid) too.
Jid registry = findRegistry();
if (jid.equals(registry)) {
return true;
}
if (usedRegistries.contains(jid)) {
return true;
}
return false;
}
public boolean isRegistry(Jid jid) {
try {
return isRegistry(jid.asBareJid());
}
catch (NoResponseException | XMPPErrorException | NotConnectedException
| InterruptedException e) {
LOGGER.log(Level.WARNING, "Could not determine if " + jid + " is a registry", e);
return false;
}
}
private void interactWithRegistry(Jid registry) throws NotConnectedException, InterruptedException {
boolean isNew = usedRegistries.add(registry);
if (!isNew) {
return;
}
IoTProvisioningManager iotProvisioningManager = IoTProvisioningManager.getInstanceFor(connection());
iotProvisioningManager.sendFriendshipRequestIfRequired(registry.asBareJid());
}
public ThingState getStateFor(Thing thing) {
return things.get(thing.getNodeInfo());
}
private ThingState getStateFor(NodeInfo nodeInfo) {
ThingState state = things.get(nodeInfo);
if (state == null) {
state = new ThingState(nodeInfo);
things.put(nodeInfo, state);
}
return state;
}
}