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                            return;
147                    }
148                }, iqResultReplyFilter);
149            }
150        }, SetActiveListFilter.INSTANCE);
151        connection.addStanzaSendingListener(new StanzaListener() {
152            @Override
153            public void processStanza(Stanza packet) throws NotConnectedException {
154                XMPPConnection connection = connection();
155                Privacy privacy = (Privacy) packet;
156                StanzaFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
157                final String defaultListName = privacy.getDefaultName();
158                final boolean declinceDefaultList = privacy.isDeclineDefaultList();
159                connection.addOneTimeSyncCallback(new StanzaListener() {
160                    @Override
161                    public void processStanza(Stanza packet) throws NotConnectedException {
162                            if (declinceDefaultList) {
163                                cachedDefaultListName = null;
164                            }
165                            else {
166                                cachedDefaultListName = defaultListName;
167                            }
168                            return;
169                    }
170                }, iqResultReplyFilter);
171            }
172        }, SetDefaultListFilter.INSTANCE);
173        connection.addSyncStanzaListener(new StanzaListener() {
174            @Override
175            public void processStanza(Stanza packet) throws NotConnectedException {
176                Privacy privacy = (Privacy) packet;
177                // If a privacy IQ result stanza has an active or default list name set, then we use that
178                // as cached list name.
179                String activeList = privacy.getActiveName();
180                if (activeList != null) {
181                    cachedActiveListName = activeList;
182                }
183                String defaultList = privacy.getDefaultName();
184                if (defaultList != null) {
185                    cachedDefaultListName = defaultList;
186                }
187            }
188        }, PRIVACY_RESULT);
189        connection.addConnectionListener(new ConnectionListener() {
190            @Override
191            public void authenticated(XMPPConnection connection, boolean resumed) {
192                // No need to reset the cache if the connection got resumed.
193                if (resumed) {
194                    return;
195                }
196                cachedActiveListName = cachedDefaultListName = null;
197            }
198        });
199
200        // XEP-0016 ยง 3.
201        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
202    }
203
204    /**
205     * Returns the PrivacyListManager instance associated with a given XMPPConnection.
206     *
207     * @param connection the connection used to look for the proper PrivacyListManager.
208     * @return the PrivacyListManager associated with a given XMPPConnection.
209     */
210    public static synchronized PrivacyListManager getInstanceFor(XMPPConnection connection) {
211        PrivacyListManager plm = INSTANCES.get(connection);
212        if (plm == null) {
213            plm = new PrivacyListManager(connection);
214            // Register the new instance and associate it with the connection
215            INSTANCES.put(connection, plm);
216        }
217        return plm;
218    }
219
220    /**
221     * Send the {@link Privacy} stanza to the server in order to know some privacy content and then
222     * waits for the answer.
223     *
224     * @param requestPrivacy is the {@link Privacy} stanza configured properly whose XML
225     *      will be sent to the server.
226     * @return a new {@link Privacy} with the data received from the server.
227     * @throws XMPPErrorException if there was an XMPP error returned.
228     * @throws NoResponseException if there was no response from the remote entity.
229     * @throws NotConnectedException if the XMPP connection is not connected.
230     * @throws InterruptedException if the calling thread was interrupted.
231     */
232    private Privacy getRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
233        // The request is a get iq type
234        requestPrivacy.setType(Privacy.Type.get);
235
236        return connection().createStanzaCollectorAndSend(requestPrivacy).nextResultOrThrow();
237    }
238
239    /**
240     * Send the {@link Privacy} stanza to the server in order to modify the server privacy and waits
241     * for the answer.
242     *
243     * @param requestPrivacy is the {@link Privacy} stanza configured properly whose xml will be
244     *        sent to the server.
245     * @return a new {@link Privacy} with the data received from the server.
246     * @throws XMPPErrorException if there was an XMPP error returned.
247     * @throws NoResponseException if there was no response from the remote entity.
248     * @throws NotConnectedException if the XMPP connection is not connected.
249     * @throws InterruptedException if the calling thread was interrupted.
250     */
251    private Stanza setRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
252        // The request is a get iq type
253        requestPrivacy.setType(Privacy.Type.set);
254
255        return connection().createStanzaCollectorAndSend(requestPrivacy).nextResultOrThrow();
256    }
257
258    /**
259     * Answer a privacy containing the list structure without {@link PrivacyItem}.
260     *
261     * @return a Privacy with the list names.
262     * @throws XMPPErrorException if there was an XMPP error returned.
263     * @throws NoResponseException if there was no response from the remote entity.
264     * @throws NotConnectedException if the XMPP connection is not connected.
265     * @throws InterruptedException if the calling thread was interrupted.
266     */
267    private Privacy getPrivacyWithListNames() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
268        // The request of the list is an empty privacy message
269        Privacy request = new Privacy();
270
271        // Send the package to the server and get the answer
272        return getRequest(request);
273    }
274
275    /**
276     * Answer the active privacy list. Returns <code>null</code> if there is no active list.
277     *
278     * @return the privacy list of the active list.
279     * @throws XMPPErrorException if there was an XMPP error returned.
280     * @throws NoResponseException if there was no response from the remote entity.
281     * @throws NotConnectedException if the XMPP connection is not connected.
282     * @throws InterruptedException if the calling thread was interrupted.
283     */
284    public PrivacyList getActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
285        Privacy privacyAnswer = this.getPrivacyWithListNames();
286        String listName = privacyAnswer.getActiveName();
287        if (StringUtils.isNullOrEmpty(listName)) {
288            return null;
289        }
290        boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getDefaultName());
291        return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));
292    }
293
294    /**
295     * Get the name of the active list.
296     *
297     * @return the name of the active list or null if there is none set.
298     * @throws NoResponseException if there was no response from the remote entity.
299     * @throws XMPPErrorException if there was an XMPP error returned.
300     * @throws NotConnectedException if the XMPP connection is not connected.
301     * @throws InterruptedException if the calling thread was interrupted.
302     * @since 4.1
303     */
304    public String getActiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
305        if (cachedActiveListName != null) {
306            return cachedActiveListName;
307        }
308        return getPrivacyWithListNames().getActiveName();
309    }
310
311    /**
312     * Answer the default privacy list. Returns <code>null</code> if there is no default list.
313     *
314     * @return the privacy list of the default list.
315     * @throws XMPPErrorException if there was an XMPP error returned.
316     * @throws NoResponseException if there was no response from the remote entity.
317     * @throws NotConnectedException if the XMPP connection is not connected.
318     * @throws InterruptedException if the calling thread was interrupted.
319     */
320    public PrivacyList getDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
321        Privacy privacyAnswer = this.getPrivacyWithListNames();
322        String listName = privacyAnswer.getDefaultName();
323        if (StringUtils.isNullOrEmpty(listName)) {
324            return null;
325        }
326        boolean isDefaultAndActive = listName.equals(privacyAnswer.getActiveName());
327        return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));
328    }
329
330    /**
331     * Get the name of the default list.
332     *
333     * @return the name of the default list or null if there is none set.
334     * @throws NoResponseException if there was no response from the remote entity.
335     * @throws XMPPErrorException if there was an XMPP error returned.
336     * @throws NotConnectedException if the XMPP connection is not connected.
337     * @throws InterruptedException if the calling thread was interrupted.
338     * @since 4.1
339     */
340    public String getDefaultListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
341        if (cachedDefaultListName != null) {
342            return cachedDefaultListName;
343        }
344        return getPrivacyWithListNames().getDefaultName();
345    }
346
347    /**
348     * Returns the name of the effective privacy list.
349     * <p>
350     * The effective privacy list is the one that is currently enforced on the connection. It's either the active
351     * privacy list, or, if the active privacy list is not set, the default privacy list.
352     * </p>
353     *
354     * @return the name of the effective privacy list or null if there is none set.
355     * @throws NoResponseException if there was no response from the remote entity.
356     * @throws XMPPErrorException if there was an XMPP error returned.
357     * @throws NotConnectedException if the XMPP connection is not connected.
358     * @throws InterruptedException if the calling thread was interrupted.
359     * @since 4.1
360     */
361    public String getEffectiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
362        String activeListName = getActiveListName();
363        if (activeListName != null) {
364            return activeListName;
365        }
366        return getDefaultListName();
367    }
368
369    /**
370     * Answer the privacy list items under listName with the allowed and blocked permissions.
371     *
372     * @param listName the name of the list to get the allowed and blocked permissions.
373     * @return a list of privacy items under the list listName.
374     * @throws XMPPErrorException if there was an XMPP error returned.
375     * @throws NoResponseException if there was no response from the remote entity.
376     * @throws NotConnectedException if the XMPP connection is not connected.
377     * @throws InterruptedException if the calling thread was interrupted.
378     */
379    private List<PrivacyItem> getPrivacyListItems(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
380        assert StringUtils.isNotEmpty(listName);
381        // The request of the list is an privacy message with an empty list
382        Privacy request = new Privacy();
383        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
384
385        // Send the package to the server and get the answer
386        Privacy privacyAnswer = getRequest(request);
387
388        return privacyAnswer.getPrivacyList(listName);
389    }
390
391    /**
392     * Answer the privacy list items under listName with the allowed and blocked permissions.
393     *
394     * @param listName the name of the list to get the allowed and blocked permissions.
395     * @return a privacy list under the list listName.
396     * @throws XMPPErrorException if there was an XMPP error returned.
397     * @throws NoResponseException if there was no response from the remote entity.
398     * @throws NotConnectedException if the XMPP connection is not connected.
399     * @throws InterruptedException if the calling thread was interrupted.
400     */
401    public PrivacyList getPrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
402        listName = StringUtils.requireNotNullNorEmpty(listName, "List name must not be null");
403        return new PrivacyList(false, false, listName, getPrivacyListItems(listName));
404    }
405
406    /**
407     * Answer every privacy list with the allowed and blocked permissions.
408     *
409     * @return an array of privacy lists.
410     * @throws XMPPErrorException if there was an XMPP error returned.
411     * @throws NoResponseException if there was no response from the remote entity.
412     * @throws NotConnectedException if the XMPP connection is not connected.
413     * @throws InterruptedException if the calling thread was interrupted.
414     */
415    public List<PrivacyList> getPrivacyLists() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
416        Privacy privacyAnswer = getPrivacyWithListNames();
417        Set<String> names = privacyAnswer.getPrivacyListNames();
418        List<PrivacyList> lists = new ArrayList<>(names.size());
419        for (String listName : names) {
420            boolean isActiveList = listName.equals(privacyAnswer.getActiveName());
421            boolean isDefaultList = listName.equals(privacyAnswer.getDefaultName());
422            lists.add(new PrivacyList(isActiveList, isDefaultList, listName,
423                            getPrivacyListItems(listName)));
424        }
425        return lists;
426    }
427
428    /**
429     * Set or change the active list to listName.
430     *
431     * @param listName the list name to set as the active one.
432     * @throws XMPPErrorException if there was an XMPP error returned.
433     * @throws NoResponseException if there was no response from the remote entity.
434     * @throws NotConnectedException if the XMPP connection is not connected.
435     * @throws InterruptedException if the calling thread was interrupted.
436     */
437    public void setActiveListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
438        // The request of the list is an privacy message with an empty list
439        Privacy request = new Privacy();
440        request.setActiveName(listName);
441
442        // Send the package to the server
443        setRequest(request);
444    }
445
446    /**
447     * Client declines the use of active lists.
448     * @throws XMPPErrorException if there was an XMPP error returned.
449     * @throws NoResponseException if there was no response from the remote entity.
450     * @throws NotConnectedException if the XMPP connection is not connected.
451     * @throws InterruptedException if the calling thread was interrupted.
452     */
453    public void declineActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
454        // The request of the list is an privacy message with an empty list
455        Privacy request = new Privacy();
456        request.setDeclineActiveList(true);
457
458        // Send the package to the server
459        setRequest(request);
460    }
461
462    /**
463     * Set or change the default list to listName.
464     *
465     * @param listName the list name to set as the default one.
466     * @throws XMPPErrorException if there was an XMPP error returned.
467     * @throws NoResponseException if there was no response from the remote entity.
468     * @throws NotConnectedException if the XMPP connection is not connected.
469     * @throws InterruptedException if the calling thread was interrupted.
470     */
471    public void setDefaultListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
472        // The request of the list is an privacy message with an empty list
473        Privacy request = new Privacy();
474        request.setDefaultName(listName);
475
476        // Send the package to the server
477        setRequest(request);
478    }
479
480    /**
481     * Client declines the use of default lists.
482     * @throws XMPPErrorException if there was an XMPP error returned.
483     * @throws NoResponseException if there was no response from the remote entity.
484     * @throws NotConnectedException if the XMPP connection is not connected.
485     * @throws InterruptedException if the calling thread was interrupted.
486     */
487    public void declineDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
488        // The request of the list is an privacy message with an empty list
489        Privacy request = new Privacy();
490        request.setDeclineDefaultList(true);
491
492        // Send the package to the server
493        setRequest(request);
494    }
495
496    /**
497     * The client has created a new list. It send the new one to the server.
498     *
499     * @param listName the list that has changed its content.
500     * @param privacyItems a List with every privacy item in the list.
501     * @throws XMPPErrorException if there was an XMPP error returned.
502     * @throws NoResponseException if there was no response from the remote entity.
503     * @throws NotConnectedException if the XMPP connection is not connected.
504     * @throws InterruptedException if the calling thread was interrupted.
505     */
506    public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
507        updatePrivacyList(listName, privacyItems);
508    }
509
510    /**
511     * The client has edited an existing list. It updates the server content with the resulting
512     * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the
513     * list (not the "delta").
514     *
515     * @param listName the list that has changed its content.
516     * @param privacyItems a List with every privacy item in the list.
517     * @throws XMPPErrorException if there was an XMPP error returned.
518     * @throws NoResponseException if there was no response from the remote entity.
519     * @throws NotConnectedException if the XMPP connection is not connected.
520     * @throws InterruptedException if the calling thread was interrupted.
521     */
522    public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
523        // Build the privacy package to add or update the new list
524        Privacy request = new Privacy();
525        request.setPrivacyList(listName, privacyItems);
526
527        // Send the package to the server
528        setRequest(request);
529    }
530
531    /**
532     * Remove a privacy list.
533     *
534     * @param listName the list that has changed its content.
535     * @throws XMPPErrorException if there was an XMPP error returned.
536     * @throws NoResponseException if there was no response from the remote entity.
537     * @throws NotConnectedException if the XMPP connection is not connected.
538     * @throws InterruptedException if the calling thread was interrupted.
539     */
540    public void deletePrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
541        // The request of the list is an privacy message with an empty list
542        Privacy request = new Privacy();
543        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
544
545        // Send the package to the server
546        setRequest(request);
547    }
548
549    /**
550     * Adds a privacy list listener that will be notified of any new update in the user
551     * privacy communication.
552     *
553     * @param listener a privacy list listener.
554     * @return true, if the listener was not already added.
555     */
556    public boolean addListener(PrivacyListListener listener) {
557        return listeners.add(listener);
558    }
559
560    /**
561     * Removes the privacy list listener.
562     *
563     * @param listener TODO javadoc me please
564     * @return true, if the listener was removed.
565     */
566    public boolean removeListener(PrivacyListListener listener) {
567        return listeners.remove(listener);
568    }
569
570    /**
571     * Check if the user's server supports privacy lists.
572     *
573     * @return true, if the server supports privacy lists, false otherwise.
574     * @throws XMPPErrorException if there was an XMPP error returned.
575     * @throws NoResponseException if there was no response from the remote entity.
576     * @throws NotConnectedException if the XMPP connection is not connected.
577     * @throws InterruptedException if the calling thread was interrupted.
578     */
579    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
580        return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE);
581    }
582}