diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java new file mode 100644 index 000000000..83d9f49fe --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java @@ -0,0 +1,26 @@ +/** + * + * 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.provisioning; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.BareJid; + +public interface BecameFriendListener { + + void becameFriend(BareJid jid, Presence presence); + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java index d9264dfca..1ef6a6811 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java @@ -18,7 +18,9 @@ package org.jivesoftware.smackx.iot.provisioning; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,6 +43,7 @@ import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.roster.AbstractPresenceEventListener; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterEntry; import org.jivesoftware.smack.roster.SubscribeListener; @@ -50,6 +53,7 @@ import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager; import org.jivesoftware.smackx.iot.provisioning.element.ClearCache; import org.jivesoftware.smackx.iot.provisioning.element.ClearCacheResponse; import org.jivesoftware.smackx.iot.provisioning.element.Constants; +import org.jivesoftware.smackx.iot.provisioning.element.Friend; import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriend; import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriendResponse; import org.jivesoftware.smackx.iot.provisioning.element.Unfriend; @@ -68,6 +72,8 @@ public final class IoTProvisioningManager extends Manager { private static final Logger LOGGER = Logger.getLogger(IoTProvisioningManager.class.getName()); + private static final StanzaFilter FRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, + new StanzaExtensionFilter(Friend.ELEMENT, Friend.NAMESPACE)); private static final StanzaFilter UNFRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(Unfriend.ELEMENT, Unfriend.NAMESPACE)); @@ -99,6 +105,13 @@ public final class IoTProvisioningManager extends Manager { private final Roster roster; private final LruCache> negativeFriendshipRequestCache = new LruCache<>(8); + private final LruCache friendshipDeniedCache = new LruCache<>(16); + + private final LruCache friendshipRequestedCache = new LruCache<>(16); + + private final Set becameFriendListeners = new CopyOnWriteArraySet<>(); + + private final Set wasUnfriendedListeners = new CopyOnWriteArraySet<>(); private Jid configuredProvisioningServer; @@ -129,6 +142,47 @@ public final class IoTProvisioningManager extends Manager { } }, UNFRIEND_MESSAGE); + // Stanza listener for XEP-0324 § 3.2.4. + connection.addAsyncStanzaListener(new StanzaListener() { + @Override + public void processPacket(final Stanza stanza) throws NotConnectedException, InterruptedException { + final Message friendMessage = (Message) stanza; + final Friend friend = Friend.from(friendMessage); + final BareJid friendJid = friend.getFriend(); + + if (isFromProvisioningService(friendMessage)) { + // We received a recommendation from a provisioning server. + // Notify the recommended friend that we will now accept his + // friendship requests. + final XMPPConnection connection = connection(); + Friend friendNotifiacation = new Friend(connection.getUser().asBareJid()); + Message notificationMessage = new Message(friendJid, friendNotifiacation); + connection.sendStanza(notificationMessage); + } else { + // Check is the message was send from a thing we previously + // tried to become friends with. If this is the case, then + // thing is likely telling us that we can become now + // friends. + Jid from = friendMessage.getFrom(); + if (!friendshipDeniedCache.containsKey(from)) { + return; + } + + BareJid bareFrom = from.asBareJid(); + // Sanity check: If a thing recommends us itself as friend, + // which should be the case once we reach this code, then + // the bare 'from' JID should be equals to the JID of the + // recommended friend. + if (!bareFrom.equals(friendJid)) { + return; + } + + // Re-try the friendship request. + sendFriendshipRequest(friendJid); + } + } + }, FRIEND_MESSAGE); + connection.registerIQRequestHandler( new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) { @Override @@ -193,6 +247,25 @@ public final class IoTProvisioningManager extends Manager { } } }); + + roster.addPresenceEventListener(new AbstractPresenceEventListener() { + @Override + public void presenceSubscribed(BareJid address, Presence subscribedPresence) { + friendshipRequestedCache.remove(address); + for (BecameFriendListener becameFriendListener : becameFriendListeners) { + becameFriendListener.becameFriend(address, subscribedPresence); + } + } + @Override + public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) { + if (friendshipRequestedCache.containsKey(address)) { + friendshipDeniedCache.put(address, null); + } + for (WasUnfriendedListener wasUnfriendedListener : wasUnfriendedListeners) { + wasUnfriendedListener.wasUnfriendedListener(address, unsubscribedPresence); + } + } + }); } /** @@ -272,6 +345,9 @@ public final class IoTProvisioningManager extends Manager { public void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException { Presence presence = new Presence(Presence.Type.subscribe); presence.setTo(bareJid); + + friendshipRequestedCache.put(bareJid, null); + connection().sendStanza(presence); } @@ -295,6 +371,22 @@ public final class IoTProvisioningManager extends Manager { } } + public boolean addBecameFriendListener(BecameFriendListener becameFriendListener) { + return becameFriendListeners.add(becameFriendListener); + } + + public boolean removeBecameFriendListener(BecameFriendListener becameFriendListener) { + return becameFriendListeners.remove(becameFriendListener); + } + + public boolean addWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { + return wasUnfriendedListeners.add(wasUnfriendedListener); + } + + public boolean removeWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { + return wasUnfriendedListeners.remove(wasUnfriendedListener); + } + private boolean isFromProvisioningService(Stanza stanza) { Jid provisioningServer; try { diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java new file mode 100644 index 000000000..43e9a6d31 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java @@ -0,0 +1,26 @@ +/** + * + * 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.provisioning; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.BareJid; + +public interface WasUnfriendedListener { + + void wasUnfriendedListener(BareJid jid, Presence presence); + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java new file mode 100644 index 000000000..224269fbd --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java @@ -0,0 +1,61 @@ +/** + * + * 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.provisioning.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jxmpp.jid.BareJid; + +public class Friend implements ExtensionElement { + + public static final String ELEMENT = "friend"; + public static final String NAMESPACE = Constants.IOT_PROVISIONING_NAMESPACE; + + private final BareJid friend; + + public Friend(BareJid friend) { + this.friend = Objects.requireNonNull(friend, "Friend must not be null"); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("jid", friend); + xml.closeEmptyElement(); + return xml; + } + + public BareJid getFriend() { + return friend; + } + + public static Friend from(Message message) { + return message.getExtension(ELEMENT, NAMESPACE); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java new file mode 100644 index 000000000..7267a75ad --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java @@ -0,0 +1,34 @@ +/** + * + * 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.provisioning.provider; + +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.iot.provisioning.element.Friend; +import org.jxmpp.jid.BareJid; +import org.jxmpp.stringprep.XmppStringprepException; +import org.xmlpull.v1.XmlPullParser; + +public class FriendProvider extends ExtensionElementProvider { + + @Override + public Friend parse(XmlPullParser parser, int initialDepth) throws XmppStringprepException { + BareJid jid = ParserUtils.getBareJidAttribute(parser); + return new Friend(jid); + } + +} diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 9d7066499..a88e9aae7 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -132,6 +132,11 @@ urn:xmpp:iot:provisioning org.jivesoftware.smackx.iot.provisioning.provider.ClearCacheResponseProvider + + friend + urn:xmpp:iot:provisioning + org.jivesoftware.smackx.iot.provisioning.provider.FriendProvider + unfriend urn:xmpp:iot:provisioning diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java index da370a0ef..c372d689d 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java @@ -681,6 +681,7 @@ public final class Roster extends Manager { Objects.requireNonNull(subscribeListener, "SubscribeListener argument must not be null"); if (subscriptionMode != SubscriptionMode.manual) { previousSubscriptionMode = subscriptionMode; + subscriptionMode = SubscriptionMode.manual; } return subscribeListeners.add(subscribeListener); } @@ -1228,6 +1229,7 @@ public final class Roster extends Manager { RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry); if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) { updatedEntries.add(item.getJid()); + oldEntry.updateItem(item); } else { // Record the entry as unchanged, so that it doesn't end up as deleted entry unchangedEntries.add(item.getJid()); diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java index a47f8a6ed..f0bb40017 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java @@ -28,6 +28,8 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jxmpp.jid.BareJid; @@ -41,7 +43,7 @@ import org.jxmpp.jid.BareJid; */ public final class RosterEntry extends Manager { - private final RosterPacket.Item item; + private RosterPacket.Item item; final private Roster roster; /** @@ -120,10 +122,9 @@ public final class RosterEntry extends Manager { * @param type the subscription type. * @param subscriptionPending TODO */ - void updateState(String name, RosterPacket.ItemType type, boolean subscriptionPending) { - item.setName(name); - item.setItemType(type); - item.setSubscriptionPending(subscriptionPending); + void updateItem(RosterPacket.Item item) { + assert(item != null); + this.item = item; } /** @@ -209,6 +210,18 @@ public final class RosterEntry extends Manager { } } + /** + * Cancel the presence subscription the XMPP entity representing this roster entry has with us. + * + * @throws NotConnectedException + * @throws InterruptedException + * @since 4.2 + */ + public void cancelSubscription() throws NotConnectedException, InterruptedException { + Presence unsubscribed = new Presence(item.getJid(), Type.unsubscribed); + connection().sendStanza(unsubscribed); + } + public String toString() { StringBuilder buf = new StringBuilder(); if (getName() != null) { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java index 1ee9cbaa8..bf1a15eff 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java @@ -25,6 +25,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; +import org.jivesoftware.smack.XMPPConnection; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; @@ -90,4 +91,24 @@ public class RosterUtil { roster.sendSubscriptionRequest(jid); } } + + public static void ensureNotSubscribedToEachOther(XMPPConnection connectionOne, XMPPConnection connectionTwo) + throws NotConnectedException, InterruptedException { + final Roster rosterOne = Roster.getInstanceFor(connectionOne); + final BareJid jidOne = connectionOne.getUser().asBareJid(); + final Roster rosterTwo = Roster.getInstanceFor(connectionTwo); + final BareJid jidTwo = connectionTwo.getUser().asBareJid(); + + ensureNotSubscribed(rosterOne, jidTwo); + ensureNotSubscribed(rosterTwo, jidOne); + } + + public static void ensureNotSubscribed(Roster roster, BareJid jid) + throws NotConnectedException, InterruptedException { + RosterEntry entry = roster.getEntry(jid); + if (entry != null && entry.canSeeMyPresence()) { + entry.cancelSubscription(); + } + } + } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java index f74ec379d..a9bd5810d 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java @@ -108,6 +108,7 @@ public class RosterPacket extends IQ { * A roster item, which consists of a JID, their name, the type of subscription, and * the groups the roster item belongs to. */ + // TODO Make this class immutable. public static class Item implements NamedElement { /** @@ -124,6 +125,7 @@ public class RosterPacket extends IQ { */ private boolean subscriptionPending; + // TODO Make immutable. private String name; private ItemType itemType = ItemType.none; private boolean approved; diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java index 4ed7c60e0..489d9b424 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java @@ -17,9 +17,11 @@ package org.igniterealtime.smack.smackrepl; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterUtil; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; @@ -41,6 +43,7 @@ import org.jivesoftware.smackx.iot.data.element.IoTFieldsExtension; import org.jivesoftware.smackx.iot.discovery.AbstractThingStateChangeListener; import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager; import org.jivesoftware.smackx.iot.discovery.ThingState; +import org.jivesoftware.smackx.iot.provisioning.BecameFriendListener; import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.EntityBareJid; @@ -51,24 +54,23 @@ public class IoT { // A 10 minute timeout. private static final long TIMEOUT = 10 * 60 * 1000; + private interface IotScenario { + void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readinThingConnection) throws XMPPException, SmackException, IOException, InterruptedException, TimeoutException, Exception; + } + public static void iotScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString, - String readingThingPassword) - throws TimeoutException, Exception { + String readingThingPassword, IotScenario scenario) throws TimeoutException, Exception { final EntityBareJid dataThingJid = JidCreate.entityBareFrom(dataThingJidString); final EntityBareJid readingThingJid = JidCreate.entityBareFrom(readingThingJidString); final XMPPTCPConnectionConfiguration dataThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder() - .setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword) - .setXmppDomain(dataThingJid.asDomainBareJid()) - .setSecurityMode(SecurityMode.disabled) - .setDebuggerEnabled(true) - .build(); - final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder() - .setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword) - .setXmppDomain(readingThingJid.asDomainBareJid()) - .setSecurityMode(SecurityMode.disabled) - .setDebuggerEnabled(true) - .build(); + .setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword) + .setXmppDomain(dataThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled) + .setDebuggerEnabled(true).build(); + final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration + .builder().setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword) + .setXmppDomain(readingThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled) + .setDebuggerEnabled(true).build(); final XMPPTCPConnection dataThingConnection = new XMPPTCPConnection(dataThingConnectionConfiguration); final XMPPTCPConnection readingThingConnection = new XMPPTCPConnection(readingThingConnectionConfiguration); @@ -76,57 +78,123 @@ public class IoT { dataThingConnection.setPacketReplyTimeout(TIMEOUT); readingThingConnection.setPacketReplyTimeout(TIMEOUT); + dataThingConnection.setUseStreamManagement(false); + readingThingConnection.setUseStreamManagement(false); + try { - iotScenario(dataThingConnection, readingThingConnection); - } - finally { + dataThingConnection.connect().login(); + readingThingConnection.connect().login(); + scenario.iotScenario(dataThingConnection, readingThingConnection); + } finally { dataThingConnection.disconnect(); readingThingConnection.disconnect(); } } - public static void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) - throws TimeoutException, Exception { - dataThingConnection.connect().login(); - readingThingConnection.connect().login(); - ThingState dataThingState = actAsDataThing(dataThingConnection); - - final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); - dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { - @Override - public void owned(BareJid jid) { - syncPoint.signal(); - } - }); - // Wait until the thing is owned. - syncPoint.waitForResult(TIMEOUT); - printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); - - // Make sure things are befriended. - IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); - readingThingProvisioningManager.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); - - Roster dataThingRoster = Roster.getInstanceFor(dataThingConnection); - RosterUtil.waitUntilOtherEntityIsSubscribed(dataThingRoster, readingThingConnection.getUser().asBareJid(), TIMEOUT); - printStatus("FRIENDSHIP ACCEPTED - Trying to read out data"); - - IoTDataManager readingThingDataManager = IoTDataManager.getInstanceFor(readingThingConnection); - List values = readingThingDataManager.requestMomentaryValuesReadOut(dataThingConnection.getUser()); - if (values.size() != 1) { - throw new IllegalStateException("Unexpected number of values returned: " + values.size()); - } - IoTFieldsExtension field = values.get(0); - printStatus("DATA READ-OUT SUCCESS: " + field.toXML()); - printStatus("IoT SCENARIO FINISHED SUCCESSFULLY"); + public static void iotReadOutScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString, + String readingThingPassword) + throws Exception { + iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, READ_OUT_SCENARIO); } + public static final IotScenario READ_OUT_SCENARIO = new IotScenario() { + @Override + public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception { + ThingState dataThingState = actAsDataThing(dataThingConnection); + + final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); + dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { + @Override + public void owned(BareJid jid) { + syncPoint.signal(); + } + }); + // Wait until the thing is owned. + syncPoint.waitForResult(TIMEOUT); + printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); + + // Make sure things are befriended. + IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); + readingThingProvisioningManager.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); + + Roster dataThingRoster = Roster.getInstanceFor(dataThingConnection); + RosterUtil.waitUntilOtherEntityIsSubscribed(dataThingRoster, readingThingConnection.getUser().asBareJid(), TIMEOUT); + printStatus("FRIENDSHIP ACCEPTED - Trying to read out data"); + + IoTDataManager readingThingDataManager = IoTDataManager.getInstanceFor(readingThingConnection); + List values = readingThingDataManager.requestMomentaryValuesReadOut(dataThingConnection.getUser()); + if (values.size() != 1) { + throw new IllegalStateException("Unexpected number of values returned: " + values.size()); + } + IoTFieldsExtension field = values.get(0); + printStatus("DATA READ-OUT SUCCESS: " + field.toXML()); + printStatus("IoT SCENARIO FINISHED SUCCESSFULLY"); + } + }; + + public static void iotOwnerApprovesFriendScenario(String dataThingJidString, String dataThingPassword, + String readingThingJidString, String readingThingPassword) throws Exception { + iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, + OWNER_APPROVES_FRIEND_SCENARIO); + } + + public static final IotScenario OWNER_APPROVES_FRIEND_SCENARIO = new IotScenario() { + @Override + public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception { + // First ensure that the two XMPP entities are not already subscribed to each other presences. + RosterUtil.ensureNotSubscribedToEachOther(dataThingConnection, readingThingConnection); + + final BareJid dataThingBareJid = dataThingConnection.getUser().asBareJid(); + final BareJid readingThingBareJid = readingThingConnection.getUser().asBareJid(); + final ThingState dataThingState = actAsDataThing(dataThingConnection); + + printStatus("WAITING for 'claimed' notification. Please claim thing now"); + final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); + dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { + @Override + public void owned(BareJid jid) { + syncPoint.signal(); + } + }); + // Wait until the thing is owned. + syncPoint.waitForResult(TIMEOUT); + printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); + + // Now, ReadingThing sends a friendship request to data thing, which + // will proxy the request to its provisioning service, which will + // likely return that both a not friends since the owner did not + // authorize the friendship yet. + final SimpleResultSyncPoint friendshipApprovedSyncPoint = new SimpleResultSyncPoint(); + final IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); + final BecameFriendListener becameFriendListener = new BecameFriendListener() { + @Override + public void becameFriend(BareJid jid, Presence presence) { + if (jid.equals(dataThingBareJid)) { + friendshipApprovedSyncPoint.signal(); + } + } + }; + readingThingProvisioningManager.addBecameFriendListener(becameFriendListener); + + try { + readingThingProvisioningManager + .sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); + friendshipApprovedSyncPoint.waitForResult(TIMEOUT); + } finally { + readingThingProvisioningManager.removeBecameFriendListener(becameFriendListener); + } + + printStatus("FRIENDSHIP APPROVED - ReadingThing " + readingThingBareJid + " is now a friend of DataThing " + dataThingBareJid); + } + }; + private static ThingState actAsDataThing(XMPPTCPConnection connection) throws XMPPException, SmackException, InterruptedException { final String key = StringUtils.randomString(12); final String sn = StringUtils.randomString(12); Thing dataThing = Thing.builder() .setKey(key) .setSerialNumber(sn) - .setManufacturer("Ignite Realtime") + .setManufacturer("IgniteRealtime") .setModel("Smack") .setVersion("0.1") .setMomentaryReadOutRequestHandler(new ThingMomentaryReadOutRequest() { @@ -153,6 +221,7 @@ public class IoT { if (args.length != 4) { throw new IllegalArgumentException(); } - iotScenario(args[0], args[1], args[2], args[3]); + iotOwnerApprovesFriendScenario(args[0], args[1], args[2], args[3]); } + }