001/**
002 *
003 * Copyright 2016-2024 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.smack.roster;
018
019import java.util.Collection;
020import java.util.Date;
021import java.util.concurrent.TimeoutException;
022import java.util.concurrent.locks.Condition;
023import java.util.concurrent.locks.Lock;
024import java.util.concurrent.locks.ReentrantLock;
025
026import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.SmackException.NotLoggedInException;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.packet.Presence;
031
032import org.jxmpp.jid.BareJid;
033import org.jxmpp.jid.Jid;
034
035public class RosterUtil {
036
037    @SuppressWarnings("JavaUtilDate")
038    public static void waitUntilOtherEntityIsSubscribed(Roster roster, BareJid otherEntity, long timeoutMillis)
039                    throws InterruptedException, TimeoutException {
040        Date deadline = new Date(System.currentTimeMillis() + timeoutMillis);
041        waitUntilOtherEntityIsSubscribed(roster, otherEntity, deadline);
042    }
043
044    public static void waitUntilOtherEntityIsSubscribed(Roster roster, final BareJid otherEntity, Date deadline)
045                    throws InterruptedException, TimeoutException {
046        final Lock lock = new ReentrantLock();
047        final Condition maybeSubscribed = lock.newCondition();
048        RosterListener rosterListener = new AbstractRosterListener() {
049            private void signal() {
050                lock.lock();
051                try {
052                    // No need to use signalAll() here.
053                    maybeSubscribed.signal();
054                }
055                finally {
056                    lock.unlock();
057                }
058            }
059
060            @Override
061            public void entriesAdded(Collection<Jid> addresses) {
062                signal();
063            }
064
065            @Override
066            public void entriesUpdated(Collection<Jid> addresses) {
067                signal();
068            }
069        };
070
071        roster.addRosterListener(rosterListener);
072
073        boolean stillWaiting = true;
074        // Using the example code pattern from Condition.awaitUntil(Date) javadoc.
075        lock.lock();
076        try {
077            while (!roster.isSubscribedToMyPresence(otherEntity)) {
078                if (!stillWaiting) {
079                    throw new TimeoutException();
080                }
081                stillWaiting = maybeSubscribed.awaitUntil(deadline);
082            }
083        }
084        finally {
085            lock.unlock();
086            // Make sure the listener is removed, so we don't leak it.
087            roster.removeRosterListener(rosterListener);
088        }
089    }
090
091    /**
092     * Pre-approve the subscription if it is required and possible.
093     *
094     * @param roster The roster which should be used for the pre-approval.
095     * @param jid The XMPP address which should be pre-approved.
096     * @throws NotLoggedInException if the XMPP connection is not authenticated.
097     * @throws NotConnectedException if the XMPP connection is not connected.
098     * @throws InterruptedException if the calling thread was interrupted.
099     * @since 4.2.2
100     */
101    public static void preApproveSubscriptionIfRequiredAndPossible(Roster roster, BareJid jid)
102            throws NotLoggedInException, NotConnectedException, InterruptedException {
103        if (!roster.isSubscriptionPreApprovalSupported()) {
104            return;
105        }
106
107        RosterEntry entry = roster.getEntry(jid);
108        if (entry == null || (!entry.canSeeMyPresence() && !entry.isApproved())) {
109            try {
110                roster.preApprove(jid);
111            } catch (FeatureNotSupportedException e) {
112                // Should never happen since we checked for the feature above.
113                throw new AssertionError(e);
114            }
115        }
116    }
117
118    public static void askForSubscriptionIfRequired(Roster roster, BareJid jid)
119            throws NotLoggedInException, NotConnectedException, InterruptedException {
120        RosterEntry entry = roster.getEntry(jid);
121        if (entry == null || !(entry.canSeeHisPresence() || entry.isSubscriptionPending())) {
122            roster.sendSubscriptionRequest(jid);
123        }
124    }
125
126    public static void ensureNotSubscribedToEachOther(XMPPConnection connectionOne, XMPPConnection connectionTwo)
127            throws NotConnectedException, InterruptedException {
128        final Roster rosterOne = Roster.getInstanceFor(connectionOne);
129        final BareJid jidOne = connectionOne.getUser().asBareJid();
130        final Roster rosterTwo = Roster.getInstanceFor(connectionTwo);
131        final BareJid jidTwo = connectionTwo.getUser().asBareJid();
132
133        ensureNotSubscribed(rosterOne, jidTwo);
134        ensureNotSubscribed(rosterTwo, jidOne);
135    }
136
137    public static void ensureNotSubscribed(Roster roster, BareJid jid)
138            throws NotConnectedException, InterruptedException {
139        RosterEntry entry = roster.getEntry(jid);
140        if (entry != null && entry.canSeeMyPresence()) {
141            entry.cancelSubscription();
142        }
143    }
144
145    public static void ensureSubscribed(XMPPConnection connectionOne, XMPPConnection connectionTwo, long timeout)
146                    throws NotLoggedInException, NotConnectedException, InterruptedException, TimeoutException {
147        ensureSubscribedTo(connectionOne, connectionTwo, timeout);
148        ensureSubscribedTo(connectionTwo, connectionOne, timeout);
149    }
150
151    @SuppressWarnings("JavaUtilDate")
152    public static void ensureSubscribedTo(XMPPConnection connectionOne, XMPPConnection connectionTwo, long timeout)
153                    throws NotLoggedInException, NotConnectedException, InterruptedException, TimeoutException {
154        Date deadline = new Date(System.currentTimeMillis() + timeout);
155        ensureSubscribedTo(connectionOne, connectionTwo, deadline);
156    }
157
158    public static void ensureSubscribedTo(final XMPPConnection connectionOne, final XMPPConnection connectionTwo,
159                    final Date deadline)
160                    throws NotLoggedInException, NotConnectedException, InterruptedException, TimeoutException {
161        final Roster rosterOne = Roster.getInstanceFor(connectionOne);
162        final BareJid jidTwo = connectionTwo.getUser().asBareJid();
163
164        if (rosterOne.iAmSubscribedTo(jidTwo))
165            return;
166
167        final BareJid jidOne = connectionOne.getUser().asBareJid();
168        final SubscribeListener subscribeListener = new SubscribeListener() {
169            @Override
170            public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) {
171                if (from.equals(jidOne)) {
172                    return SubscribeAnswer.Approve;
173                }
174                return null;
175            }
176        };
177        final Roster rosterTwo = Roster.getInstanceFor(connectionTwo);
178
179        rosterTwo.addSubscribeListener(subscribeListener);
180        try {
181            rosterOne.sendSubscriptionRequest(jidTwo);
182            waitUntilOtherEntityIsSubscribed(rosterTwo, jidOne, deadline);
183        }
184        finally {
185            rosterTwo.removeSubscribeListener(subscribeListener);
186        }
187    }
188}