diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/AbstractRosterListener.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/AbstractRosterListener.java new file mode 100644 index 000000000..95825207c --- /dev/null +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/AbstractRosterListener.java @@ -0,0 +1,47 @@ +/** + * + * Copyright 2015 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.smack.roster; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.Jid; + +import java.util.Collection; + +/** + * Provides empty implementations for {@link RosterListener}. + * + * @since 4.2 + */ +public abstract class AbstractRosterListener implements RosterListener { + + @Override + public void entriesAdded(Collection addresses) { + } + + @Override + public void entriesUpdated(Collection addresses) { + } + + @Override + public void entriesDeleted(Collection addresses) { + } + + @Override + public void presenceChanged(Presence presence) { + } +} 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 5673f5012..53bbaa313 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 @@ -46,6 +46,7 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; @@ -55,6 +56,7 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.packet.XMPPError.Condition; +import org.jivesoftware.smack.roster.SubscribeListener.SubscribeAnswer; import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smack.roster.packet.RosterVer; import org.jivesoftware.smack.roster.packet.RosterPacket.Item; @@ -169,6 +171,8 @@ public final class Roster extends Manager { private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); + private SubscribeListener subscribeListener; + /** * Returns the default subscription processing mode to use when a new Roster is created. The * subscription processing mode dictates what action Smack will take when subscription @@ -208,6 +212,46 @@ public final class Roster extends Manager { // Listen for any presence packets. connection.addSyncStanzaListener(presencePacketListener, PRESENCE_PACKET_FILTER); + connection.addAsyncStanzaListener(new StanzaListener() { + @Override + public void processPacket(Stanza stanza) throws NotConnectedException, + InterruptedException { + Presence presence = (Presence) stanza; + Jid from = presence.getFrom(); + SubscribeAnswer subscribeAnswer = null; + switch (subscriptionMode) { + case manual: + final SubscribeListener subscribeListener = Roster.this.subscribeListener; + if (subscribeListener == null) { + return; + } + subscribeAnswer = subscribeListener.processSubscribe(from, presence); + if (subscribeAnswer == null) { + return; + } + break; + case accept_all: + // Accept all subscription requests. + subscribeAnswer = SubscribeAnswer.Approve; + break; + case reject_all: + // Reject all subscription requests. + subscribeAnswer = SubscribeAnswer.Deny; + break; + } + + Presence response; + if (subscribeAnswer == SubscribeAnswer.Approve) { + response = new Presence(Presence.Type.subscribed); + } + else { + response = new Presence(Presence.Type.unsubscribed); + } + response.setTo(presence.getFrom()); + connection.sendStanza(response); + } + }, PresenceTypeFilter.SUBSCRIBE); + // Listen for connection events connection.addConnectionListener(new AbstractConnectionClosedListener() { @@ -530,6 +574,22 @@ public final class Roster extends Manager { return connection.hasFeature(SubscriptionPreApproval.ELEMENT, SubscriptionPreApproval.NAMESPACE); } + /** + * Set the subscribe listener, which is invoked on incoming subscription requests and if + * {@link SubscriptionMode} is set to {@link SubscriptionMode#manual}. If + * subscribeListener is not null, then this also sets subscription + * mode to {@link SubscriptionMode#manual}. + * + * @param subscribeListener the subscribe listener to set. + * @since 4.2 + */ + public void setSubscribeListener(SubscribeListener subscribeListener) { + if (subscribeListener != null) { + setSubscriptionMode(SubscriptionMode.manual); + } + this.subscribeListener = subscribeListener; + } + /** * Removes a roster entry from the roster. The roster entry will also be removed from the * unfiled entries or from any roster group where it could belong and will no longer be part @@ -1230,7 +1290,6 @@ public final class Roster extends Manager { @Override public void processPacket(Stanza packet) throws NotConnectedException, InterruptedException { - final XMPPConnection connection = connection(); Presence presence = (Presence) packet; Jid from = presence.getFrom(); Resourcepart fromResource = Resourcepart.EMPTY; @@ -1242,7 +1301,6 @@ public final class Roster extends Manager { } Jid key = getMapKey(from); Map userPresences; - Presence response = null; // If an "available" presence, add it to the presence map. Each presence // map will hold for a particular user a map with the presence @@ -1282,37 +1340,6 @@ public final class Roster extends Manager { fireRosterPresenceEvent(presence); } break; - case subscribe: - switch (subscriptionMode) { - case accept_all: - // Accept all subscription requests. - response = new Presence(Presence.Type.subscribed); - break; - case reject_all: - // Reject all subscription requests. - response = new Presence(Presence.Type.unsubscribed); - break; - case manual: - default: - // Otherwise, in manual mode so ignore. - break; - } - if (response != null) { - response.setTo(presence.getFrom()); - connection.sendStanza(response); - } - break; - case unsubscribe: - if (subscriptionMode != SubscriptionMode.manual) { - // Acknowledge and accept unsubscription notification so that the - // server will stop sending notifications saying that the contact - // has unsubscribed to our presence. - response = new Presence(Presence.Type.unsubscribed); - response.setTo(presence.getFrom()); - connection.sendStanza(response); - } - // Otherwise, in manual mode so ignore. - break; // Error presence packets from a bare JID mean we invalidate all existing // presence info for the user. case error: diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/SubscribeListener.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/SubscribeListener.java new file mode 100644 index 000000000..c9b61516c --- /dev/null +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/SubscribeListener.java @@ -0,0 +1,44 @@ +/** + * + * Copyright 2015 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.smack.roster; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.Jid; + + +/** + * Handle incoming requests to subscribe to our presence. + * + */ +public interface SubscribeListener { + + public enum SubscribeAnswer { + Approve, + Deny, + } + + /** + * Handle incoming presence subscription requests. + * + * @param from the JID requesting the subscription. + * @param subscribeRequest the presence stanza used for the request. + * @return a answer to the request, or null + */ + public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest); + +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/SimpleResultSyncPoint.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/SimpleResultSyncPoint.java new file mode 100644 index 000000000..d9baeff0f --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/util/SimpleResultSyncPoint.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 2015 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.igniterealtime.smack.inttest.util; + +public class SimpleResultSyncPoint extends ResultSyncPoint { + + public void signal() { + signal(Boolean.TRUE); + } + + public void signalFailure() { + signalFailure("Unspecified failure"); + } + + public void signalFailure(String failureMessage) { + signal(new Exception(failureMessage)); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java new file mode 100644 index 000000000..6618b7a3e --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java @@ -0,0 +1,112 @@ +/** + * + * Copyright 2015 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.smack.roster; + +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.concurrent.TimeoutException; + +import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackException.NotLoggedInException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.roster.packet.RosterPacket.ItemType; +import org.jxmpp.jid.Jid; + +public class RosterIntegrationTest extends AbstractSmackIntegrationTest { + + private final Roster rosterOne; + private final Roster rosterTwo; + + public RosterIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment); + rosterOne = Roster.getInstanceFor(conOne); + rosterTwo = Roster.getInstanceFor(conTwo); + } + + @SmackIntegrationTest + public void subscribeRequestListenerTest() throws TimeoutException, Exception { + ensureBothAccountsAreNotInEachOthersRoster(); + + rosterTwo.setSubscribeListener(new SubscribeListener() { + @Override + public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { + if (from.equals(conOne.getUser().asBareJid())) { + return SubscribeAnswer.Approve; + } + return SubscribeAnswer.Deny; + } + }); + + final String conTwosRosterName = "ConTwo " + testRunId; + final SimpleResultSyncPoint addedAndSubscribed = new SimpleResultSyncPoint(); + rosterOne.addRosterListener(new AbstractRosterListener() { + @Override + public void entriesAdded(Collection addresses) { + checkIfAddedAndSubscribed(addresses); + } + @Override + public void entriesUpdated(Collection addresses) { + checkIfAddedAndSubscribed(addresses); + } + private void checkIfAddedAndSubscribed(Collection addresses) { + for (Jid jid : addresses) { + if (!jid.equals(conTwo.getUser().asBareJidString())) { + continue; + } + RosterEntry rosterEntry = rosterOne.getEntry(conTwo.getUser().asBareJid()); + if (!rosterEntry.getName().equals(conTwosRosterName)) { + addedAndSubscribed.signalFailure("Roster name does not match"); + return; + } + if (!rosterEntry.getType().equals(ItemType.to)) { + return; + } + addedAndSubscribed.signal(); + } + } + }); + rosterOne.createEntry(conTwo.getUser().asBareJid(), conTwosRosterName, null); + + assertTrue(addedAndSubscribed.waitForResult(2 * connection.getPacketReplyTimeout())); + } + + private void ensureBothAccountsAreNotInEachOthersRoster() throws NotLoggedInException, + NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + notInRoster(conOne, conTwo); + notInRoster(conTwo, conOne); + } + + private void notInRoster(XMPPConnection c1, XMPPConnection c2) throws NotLoggedInException, + NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + Roster roster = Roster.getInstanceFor(c1); + RosterEntry c2Entry = roster.getEntry(c2.getUser().asBareJid()); + if (c2Entry == null) { + return; + } + roster.removeEntry(c2Entry); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/package-info.java new file mode 120000 index 000000000..a484788a7 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-im/src/main/java/org/jivesoftware/smack/roster/package-info.java \ No newline at end of file