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