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}