001/**
002 *
003 * Copyright 2006-2007 Jive Software.
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.privacy;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.WeakHashMap;
024import java.util.concurrent.CopyOnWriteArraySet;
025
026import org.jivesoftware.smack.ConnectionCreationListener;
027import org.jivesoftware.smack.ConnectionListener;
028import org.jivesoftware.smack.Manager;
029import org.jivesoftware.smack.SmackException.NoResponseException;
030import org.jivesoftware.smack.SmackException.NotConnectedException;
031import org.jivesoftware.smack.StanzaListener;
032import org.jivesoftware.smack.XMPPConnection;
033import org.jivesoftware.smack.XMPPConnectionRegistry;
034import org.jivesoftware.smack.XMPPException.XMPPErrorException;
035import org.jivesoftware.smack.filter.AndFilter;
036import org.jivesoftware.smack.filter.IQResultReplyFilter;
037import org.jivesoftware.smack.filter.IQTypeFilter;
038import org.jivesoftware.smack.filter.StanzaFilter;
039import org.jivesoftware.smack.filter.StanzaTypeFilter;
040import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
041import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
042import org.jivesoftware.smack.packet.IQ;
043import org.jivesoftware.smack.packet.Stanza;
044import org.jivesoftware.smack.util.StringUtils;
045
046import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
047import org.jivesoftware.smackx.privacy.filter.SetActiveListFilter;
048import org.jivesoftware.smackx.privacy.filter.SetDefaultListFilter;
049import org.jivesoftware.smackx.privacy.packet.Privacy;
050import org.jivesoftware.smackx.privacy.packet.PrivacyItem;
051
052/**
053 * A PrivacyListManager is used by XMPP clients to block or allow communications from other
054 * users. Use the manager to:
055 * <ul>
056 *      <li>Retrieve privacy lists.
057 *      <li>Add, remove, and edit privacy lists.
058 *      <li>Set, change, or decline active lists.
059 *      <li>Set, change, or decline the default list (i.e., the list that is active by default).
060 * </ul>
061 * Privacy Items can handle different kind of permission communications based on JID, group,
062 * subscription type or globally (see {@link PrivacyItem}).
063 *
064 * @author Francisco Vives
065 * @see <a href="http://xmpp.org/extensions/xep-0016.html">XEP-16: Privacy Lists</a>
066 */
067public final class PrivacyListManager extends Manager {
068    public static final String NAMESPACE = Privacy.NAMESPACE;
069
070    public static final StanzaFilter PRIVACY_FILTER = new StanzaTypeFilter(Privacy.class);
071
072    private static final StanzaFilter PRIVACY_RESULT = new AndFilter(IQTypeFilter.RESULT, PRIVACY_FILTER);
073
074    // Keep the list of instances of this class.
075    private static final Map<XMPPConnection, PrivacyListManager> INSTANCES = new WeakHashMap<>();
076
077    private final Set<PrivacyListListener> listeners = new CopyOnWriteArraySet<>();
078
079    static {
080        // Create a new PrivacyListManager on every established connection.
081        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
082            @Override
083            public void connectionCreated(XMPPConnection connection) {
084                getInstanceFor(connection);
085            }
086        });
087    }
088
089    // TODO implement: private final Map<String, PrivacyList> cachedPrivacyLists = new HashMap<>();
090    private volatile String cachedActiveListName;
091    private volatile String cachedDefaultListName;
092
093    /**
094     * Creates a new privacy manager to maintain the communication privacy. Note: no
095     * information is sent to or received from the server until you attempt to
096     * get or set the privacy communication.<p>
097     *
098     * @param connection the XMPP connection.
099     */
100    private PrivacyListManager(XMPPConnection connection) {
101        super(connection);
102
103        connection.registerIQRequestHandler(new AbstractIqRequestHandler(Privacy.ELEMENT, Privacy.NAMESPACE,
104                        IQ.Type.set, Mode.sync) {
105            @Override
106            public IQ handleIQRequest(IQ iqRequest) {
107                Privacy privacy = (Privacy) iqRequest;
108
109                // Notifies the event to the listeners.
110                for (PrivacyListListener listener : listeners) {
111                    // Notifies the created or updated privacy lists
112                    for (Map.Entry<String, List<PrivacyItem>> entry : privacy.getItemLists().entrySet()) {
113                        String listName = entry.getKey();
114                        List<PrivacyItem> items = entry.getValue();
115                        if (items.isEmpty()) {
116                            listener.updatedPrivacyList(listName);
117                        }
118                        else {
119                            listener.setPrivacyList(listName, items);
120                        }
121                    }
122                }
123
124                return IQ.createResultIQ(privacy);
125            }
126        });
127
128        // cached(Active|Default)ListName handling
129        connection.addStanzaSendingListener(new StanzaListener() {
130            @Override
131            public void processStanza(Stanza packet) throws NotConnectedException {
132                XMPPConnection connection = connection();
133                Privacy privacy = (Privacy) packet;
134                StanzaFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
135                final String activeListName = privacy.getActiveName();
136                final boolean declinceActiveList = privacy.isDeclineActiveList();
137                connection.addOneTimeSyncCallback(new StanzaListener() {
138                    @Override
139                    public void processStanza(Stanza packet) throws NotConnectedException {
140                            if (declinceActiveList) {
141                                cachedActiveListName = null;
142                            }
143                            else {
144                                cachedActiveListName = activeListName;
145                            }
146                    }
147                }, iqResultReplyFilter);
148            }
149        }, SetActiveListFilter.INSTANCE);
150        connection.addStanzaSendingListener(new StanzaListener() {
151            @Override
152            public void processStanza(Stanza packet) throws NotConnectedException {
153                XMPPConnection connection = connection();
154                Privacy privacy = (Privacy) packet;
155                StanzaFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
156                final String defaultListName = privacy.getDefaultName();
157                final boolean declinceDefaultList = privacy.isDeclineDefaultList();
158                connection.addOneTimeSyncCallback(new StanzaListener() {
159                    @Override
160                    public void processStanza(Stanza packet) throws NotConnectedException {
161                            if (declinceDefaultList) {
162                                cachedDefaultListName = null;
163                            }
164                            else {
165                                cachedDefaultListName = defaultListName;
166                            }
167                    }
168                }, iqResultReplyFilter);
169            }
170        }, SetDefaultListFilter.INSTANCE);
171        connection.addSyncStanzaListener(new StanzaListener() {
172            @Override
173            public void processStanza(Stanza packet) throws NotConnectedException {
174                Privacy privacy = (Privacy) packet;
175                // If a privacy IQ result stanza has an active or default list name set, then we use that
176                // as cached list name.
177                String activeList = privacy.getActiveName();
178                if (activeList != null) {
179                    cachedActiveListName = activeList;
180                }
181                String defaultList = privacy.getDefaultName();
182                if (defaultList != null) {
183                    cachedDefaultListName = defaultList;
184                }
185            }
186        }, PRIVACY_RESULT);
187        connection.addConnectionListener(new ConnectionListener() {
188            @Override
189            public void authenticated(XMPPConnection connection, boolean resumed) {
190                // No need to reset the cache if the connection got resumed.
191                if (resumed) {
192                    return;
193                }
194                cachedActiveListName = cachedDefaultListName = null;
195            }
196        });
197
198        // XEP-0016 ยง 3.
199        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
200    }
201
202    /**
203     * Returns the PrivacyListManager instance associated with a given XMPPConnection.
204     *
205     * @param connection the connection used to look for the proper PrivacyListManager.
206     * @return the PrivacyListManager associated with a given XMPPConnection.
207     */
208    public static synchronized PrivacyListManager getInstanceFor(XMPPConnection connection) {
209        PrivacyListManager plm = INSTANCES.get(connection);
210        if (plm == null) {
211            plm = new PrivacyListManager(connection);
212            // Register the new instance and associate it with the connection
213            INSTANCES.put(connection, plm);
214        }
215        return plm;
216    }
217
218    /**
219     * Send the {@link Privacy} stanza to the server in order to know some privacy content and then
220     * waits for the answer.
221     *
222     * @param requestPrivacy is the {@link Privacy} stanza configured properly whose XML
223     *      will be sent to the server.
224     * @return a new {@link Privacy} with the data received from the server.
225     * @throws XMPPErrorException if there was an XMPP error returned.
226     * @throws NoResponseException if there was no response from the remote entity.
227     * @throws NotConnectedException if the XMPP connection is not connected.
228     * @throws InterruptedException if the calling thread was interrupted.
229     */
230    private Privacy getRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
231        // The request is a get iq type
232        requestPrivacy.setType(Privacy.Type.get);
233
234        return connection().sendIqRequestAndWaitForResponse(requestPrivacy);
235    }
236
237    /**
238     * Send the {@link Privacy} stanza to the server in order to modify the server privacy and waits
239     * for the answer.
240     *
241     * @param requestPrivacy is the {@link Privacy} stanza configured properly whose xml will be
242     *        sent to the server.
243     * @return a new {@link Privacy} with the data received from the server.
244     * @throws XMPPErrorException if there was an XMPP error returned.
245     * @throws NoResponseException if there was no response from the remote entity.
246     * @throws NotConnectedException if the XMPP connection is not connected.
247     * @throws InterruptedException if the calling thread was interrupted.
248     */
249    private Stanza setRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
250        // The request is a get iq type
251        requestPrivacy.setType(Privacy.Type.set);
252
253        return connection().sendIqRequestAndWaitForResponse(requestPrivacy);
254    }
255
256    /**
257     * Answer a privacy containing the list structure without {@link PrivacyItem}.
258     *
259     * @return a Privacy with the list names.
260     * @throws XMPPErrorException if there was an XMPP error returned.
261     * @throws NoResponseException if there was no response from the remote entity.
262     * @throws NotConnectedException if the XMPP connection is not connected.
263     * @throws InterruptedException if the calling thread was interrupted.
264     */
265    private Privacy getPrivacyWithListNames() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
266        // The request of the list is an empty privacy message
267        Privacy request = new Privacy();
268
269        // Send the package to the server and get the answer
270        return getRequest(request);
271    }
272
273    /**
274     * Answer the active privacy list. Returns <code>null</code> if there is no active list.
275     *
276     * @return the privacy list of the active list.
277     * @throws XMPPErrorException if there was an XMPP error returned.
278     * @throws NoResponseException if there was no response from the remote entity.
279     * @throws NotConnectedException if the XMPP connection is not connected.
280     * @throws InterruptedException if the calling thread was interrupted.
281     */
282    public PrivacyList getActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
283        Privacy privacyAnswer = this.getPrivacyWithListNames();
284        String listName = privacyAnswer.getActiveName();
285        if (StringUtils.isNullOrEmpty(listName)) {
286            return null;
287        }
288        boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getDefaultName());
289        return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));
290    }
291
292    /**
293     * Get the name of the active list.
294     *
295     * @return the name of the active list or null if there is none set.
296     * @throws NoResponseException if there was no response from the remote entity.
297     * @throws XMPPErrorException if there was an XMPP error returned.
298     * @throws NotConnectedException if the XMPP connection is not connected.
299     * @throws InterruptedException if the calling thread was interrupted.
300     * @since 4.1
301     */
302    public String getActiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
303        if (cachedActiveListName != null) {
304            return cachedActiveListName;
305        }
306        return getPrivacyWithListNames().getActiveName();
307    }
308
309    /**
310     * Answer the default privacy list. Returns <code>null</code> if there is no default list.
311     *
312     * @return the privacy list of the default list.
313     * @throws XMPPErrorException if there was an XMPP error returned.
314     * @throws NoResponseException if there was no response from the remote entity.
315     * @throws NotConnectedException if the XMPP connection is not connected.
316     * @throws InterruptedException if the calling thread was interrupted.
317     */
318    public PrivacyList getDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
319        Privacy privacyAnswer = this.getPrivacyWithListNames();
320        String listName = privacyAnswer.getDefaultName();
321        if (StringUtils.isNullOrEmpty(listName)) {
322            return null;
323        }
324        boolean isDefaultAndActive = listName.equals(privacyAnswer.getActiveName());
325        return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));
326    }
327
328    /**
329     * Get the name of the default list.
330     *
331     * @return the name of the default list or null if there is none set.
332     * @throws NoResponseException if there was no response from the remote entity.
333     * @throws XMPPErrorException if there was an XMPP error returned.
334     * @throws NotConnectedException if the XMPP connection is not connected.
335     * @throws InterruptedException if the calling thread was interrupted.
336     * @since 4.1
337     */
338    public String getDefaultListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
339        if (cachedDefaultListName != null) {
340            return cachedDefaultListName;
341        }
342        return getPrivacyWithListNames().getDefaultName();
343    }
344
345    /**
346     * Returns the name of the effective privacy list.
347     * <p>
348     * The effective privacy list is the one that is currently enforced on the connection. It's either the active
349     * privacy list, or, if the active privacy list is not set, the default privacy list.
350     * </p>
351     *
352     * @return the name of the effective privacy list or null if there is none set.
353     * @throws NoResponseException if there was no response from the remote entity.
354     * @throws XMPPErrorException if there was an XMPP error returned.
355     * @throws NotConnectedException if the XMPP connection is not connected.
356     * @throws InterruptedException if the calling thread was interrupted.
357     * @since 4.1
358     */
359    public String getEffectiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
360        String activeListName = getActiveListName();
361        if (activeListName != null) {
362            return activeListName;
363        }
364        return getDefaultListName();
365    }
366
367    /**
368     * Answer the privacy list items under listName with the allowed and blocked permissions.
369     *
370     * @param listName the name of the list to get the allowed and blocked permissions.
371     * @return a list of privacy items under the list listName.
372     * @throws XMPPErrorException if there was an XMPP error returned.
373     * @throws NoResponseException if there was no response from the remote entity.
374     * @throws NotConnectedException if the XMPP connection is not connected.
375     * @throws InterruptedException if the calling thread was interrupted.
376     */
377    private List<PrivacyItem> getPrivacyListItems(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
378        assert StringUtils.isNotEmpty(listName);
379        // The request of the list is an privacy message with an empty list
380        Privacy request = new Privacy();
381        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
382
383        // Send the package to the server and get the answer
384        Privacy privacyAnswer = getRequest(request);
385
386        return privacyAnswer.getPrivacyList(listName);
387    }
388
389    /**
390     * Answer the privacy list items under listName with the allowed and blocked permissions.
391     *
392     * @param listName the name of the list to get the allowed and blocked permissions.
393     * @return a privacy list under the list listName.
394     * @throws XMPPErrorException if there was an XMPP error returned.
395     * @throws NoResponseException if there was no response from the remote entity.
396     * @throws NotConnectedException if the XMPP connection is not connected.
397     * @throws InterruptedException if the calling thread was interrupted.
398     */
399    public PrivacyList getPrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
400        listName = StringUtils.requireNotNullNorEmpty(listName, "List name must not be null");
401        return new PrivacyList(false, false, listName, getPrivacyListItems(listName));
402    }
403
404    /**
405     * Answer every privacy list with the allowed and blocked permissions.
406     *
407     * @return an array of privacy lists.
408     * @throws XMPPErrorException if there was an XMPP error returned.
409     * @throws NoResponseException if there was no response from the remote entity.
410     * @throws NotConnectedException if the XMPP connection is not connected.
411     * @throws InterruptedException if the calling thread was interrupted.
412     */
413    public List<PrivacyList> getPrivacyLists() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
414        Privacy privacyAnswer = getPrivacyWithListNames();
415        Set<String> names = privacyAnswer.getPrivacyListNames();
416        List<PrivacyList> lists = new ArrayList<>(names.size());
417        for (String listName : names) {
418            boolean isActiveList = listName.equals(privacyAnswer.getActiveName());
419            boolean isDefaultList = listName.equals(privacyAnswer.getDefaultName());
420            lists.add(new PrivacyList(isActiveList, isDefaultList, listName,
421                            getPrivacyListItems(listName)));
422        }
423        return lists;
424    }
425
426    /**
427     * Set or change the active list to listName.
428     *
429     * @param listName the list name to set as the active one.
430     * @throws XMPPErrorException if there was an XMPP error returned.
431     * @throws NoResponseException if there was no response from the remote entity.
432     * @throws NotConnectedException if the XMPP connection is not connected.
433     * @throws InterruptedException if the calling thread was interrupted.
434     */
435    public void setActiveListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
436        // The request of the list is an privacy message with an empty list
437        Privacy request = new Privacy();
438        request.setActiveName(listName);
439
440        // Send the package to the server
441        setRequest(request);
442    }
443
444    /**
445     * Client declines the use of active lists.
446     * @throws XMPPErrorException if there was an XMPP error returned.
447     * @throws NoResponseException if there was no response from the remote entity.
448     * @throws NotConnectedException if the XMPP connection is not connected.
449     * @throws InterruptedException if the calling thread was interrupted.
450     */
451    public void declineActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
452        // The request of the list is an privacy message with an empty list
453        Privacy request = new Privacy();
454        request.setDeclineActiveList(true);
455
456        // Send the package to the server
457        setRequest(request);
458    }
459
460    /**
461     * Set or change the default list to listName.
462     *
463     * @param listName the list name to set as the default one.
464     * @throws XMPPErrorException if there was an XMPP error returned.
465     * @throws NoResponseException if there was no response from the remote entity.
466     * @throws NotConnectedException if the XMPP connection is not connected.
467     * @throws InterruptedException if the calling thread was interrupted.
468     */
469    public void setDefaultListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
470        // The request of the list is an privacy message with an empty list
471        Privacy request = new Privacy();
472        request.setDefaultName(listName);
473
474        // Send the package to the server
475        setRequest(request);
476    }
477
478    /**
479     * Client declines the use of default lists.
480     * @throws XMPPErrorException if there was an XMPP error returned.
481     * @throws NoResponseException if there was no response from the remote entity.
482     * @throws NotConnectedException if the XMPP connection is not connected.
483     * @throws InterruptedException if the calling thread was interrupted.
484     */
485    public void declineDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
486        // The request of the list is an privacy message with an empty list
487        Privacy request = new Privacy();
488        request.setDeclineDefaultList(true);
489
490        // Send the package to the server
491        setRequest(request);
492    }
493
494    /**
495     * The client has created a new list. It send the new one to the server.
496     *
497     * @param listName the list that has changed its content.
498     * @param privacyItems a List with every privacy item in the list.
499     * @throws XMPPErrorException if there was an XMPP error returned.
500     * @throws NoResponseException if there was no response from the remote entity.
501     * @throws NotConnectedException if the XMPP connection is not connected.
502     * @throws InterruptedException if the calling thread was interrupted.
503     */
504    public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
505        updatePrivacyList(listName, privacyItems);
506    }
507
508    /**
509     * The client has edited an existing list. It updates the server content with the resulting
510     * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the
511     * list (not the "delta").
512     *
513     * @param listName the list that has changed its content.
514     * @param privacyItems a List with every privacy item in the list.
515     * @throws XMPPErrorException if there was an XMPP error returned.
516     * @throws NoResponseException if there was no response from the remote entity.
517     * @throws NotConnectedException if the XMPP connection is not connected.
518     * @throws InterruptedException if the calling thread was interrupted.
519     */
520    public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
521        // Build the privacy package to add or update the new list
522        Privacy request = new Privacy();
523        request.setPrivacyList(listName, privacyItems);
524
525        // Send the package to the server
526        setRequest(request);
527    }
528
529    /**
530     * Remove a privacy list.
531     *
532     * @param listName the list that has changed its content.
533     * @throws XMPPErrorException if there was an XMPP error returned.
534     * @throws NoResponseException if there was no response from the remote entity.
535     * @throws NotConnectedException if the XMPP connection is not connected.
536     * @throws InterruptedException if the calling thread was interrupted.
537     */
538    public void deletePrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
539        // The request of the list is an privacy message with an empty list
540        Privacy request = new Privacy();
541        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
542
543        // Send the package to the server
544        setRequest(request);
545    }
546
547    /**
548     * Adds a privacy list listener that will be notified of any new update in the user
549     * privacy communication.
550     *
551     * @param listener a privacy list listener.
552     * @return true, if the listener was not already added.
553     */
554    public boolean addListener(PrivacyListListener listener) {
555        return listeners.add(listener);
556    }
557
558    /**
559     * Removes the privacy list listener.
560     *
561     * @param listener TODO javadoc me please
562     * @return true, if the listener was removed.
563     */
564    public boolean removeListener(PrivacyListListener listener) {
565        return listeners.remove(listener);
566    }
567
568    /**
569     * Check if the user's server supports privacy lists.
570     *
571     * @return true, if the server supports privacy lists, false otherwise.
572     * @throws XMPPErrorException if there was an XMPP error returned.
573     * @throws NoResponseException if there was no response from the remote entity.
574     * @throws NotConnectedException if the XMPP connection is not connected.
575     * @throws InterruptedException if the calling thread was interrupted.
576     */
577    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
578        return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE);
579    }
580}