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}