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.sendIqRequestAndWaitForResponse(iotRegister);
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().sendIqRequestAndWaitForResponse(iotMine);
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().sendIqRequestAndWaitForResponse(iotRemove);
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().sendIqRequestAndWaitForResponse(iotUnregister);
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().sendIqRequestAndWaitForResponse(iotDisown);
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}