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 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            public void connectionCreated(XMPPConnection connection) {
082                getInstanceFor(connection);
083            }
084        });
085    }
086
087    // TODO implement: private final Map<String, PrivacyList> cachedPrivacyLists = new HashMap<>();
088    private volatile String cachedActiveListName;
089    private volatile String cachedDefaultListName;
090
091    /**
092     * Creates a new privacy manager to maintain the communication privacy. Note: no
093     * information is sent to or received from the server until you attempt to 
094     * get or set the privacy communication.<p>
095     *
096     * @param connection the XMPP connection.
097     */
098        private PrivacyListManager(XMPPConnection connection) {
099        super(connection);
100
101        connection.registerIQRequestHandler(new AbstractIqRequestHandler(Privacy.ELEMENT, Privacy.NAMESPACE,
102                        IQ.Type.set, Mode.sync) {
103            @Override
104            public IQ handleIQRequest(IQ iqRequest) {
105                Privacy privacy = (Privacy) iqRequest;
106
107                // Notifies the event to the listeners.
108                for (PrivacyListListener listener : listeners) {
109                    // Notifies the created or updated privacy lists
110                    for (Map.Entry<String, List<PrivacyItem>> entry : privacy.getItemLists().entrySet()) {
111                        String listName = entry.getKey();
112                        List<PrivacyItem> items = entry.getValue();
113                        if (items.isEmpty()) {
114                            listener.updatedPrivacyList(listName);
115                        }
116                        else {
117                            listener.setPrivacyList(listName, items);
118                        }
119                    }
120                }
121
122                return IQ.createResultIQ(privacy);
123            }
124        });
125
126        // cached(Active|Default)ListName handling
127        connection.addPacketSendingListener(new StanzaListener() {
128            @Override
129            public void processPacket(Stanza packet) throws NotConnectedException {
130                XMPPConnection connection = connection();
131                Privacy privacy = (Privacy) packet;
132                StanzaFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
133                final String activeListName = privacy.getActiveName();
134                final boolean declinceActiveList = privacy.isDeclineActiveList();
135                connection.addOneTimeSyncCallback(new StanzaListener() {
136                    @Override
137                    public void processPacket(Stanza packet) throws NotConnectedException {
138                            if (declinceActiveList) {
139                                cachedActiveListName = null;
140                            }
141                            else {
142                                cachedActiveListName = activeListName;
143                            }
144                            return;
145                    }
146                }, iqResultReplyFilter);
147            }
148        }, SetActiveListFilter.INSTANCE);
149        connection.addPacketSendingListener(new StanzaListener() {
150            @Override
151            public void processPacket(Stanza packet) throws NotConnectedException {
152                XMPPConnection connection = connection();
153                Privacy privacy = (Privacy) packet;
154                StanzaFilter iqResultReplyFilter = new IQResultReplyFilter(privacy, connection);
155                final String defaultListName = privacy.getDefaultName();
156                final boolean declinceDefaultList = privacy.isDeclineDefaultList();
157                connection.addOneTimeSyncCallback(new StanzaListener() {
158                    @Override
159                    public void processPacket(Stanza packet) throws NotConnectedException {
160                            if (declinceDefaultList) {
161                                cachedDefaultListName = null;
162                            }
163                            else {
164                                cachedDefaultListName = defaultListName;
165                            }
166                            return;
167                    }
168                }, iqResultReplyFilter);
169            }
170        }, SetDefaultListFilter.INSTANCE);
171        connection.addSyncStanzaListener(new StanzaListener() {
172            @Override
173            public void processPacket(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 AbstractConnectionListener() {
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(/packet) 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(/packet) 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 
226         * @throws NoResponseException 
227         * @throws NotConnectedException 
228         */ 
229        private Privacy getRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException  {
230                // The request is a get iq type
231                requestPrivacy.setType(Privacy.Type.get);
232
233        return connection().createPacketCollectorAndSend(requestPrivacy).nextResultOrThrow();
234        }
235
236    /**
237     * Send the {@link Privacy} stanza(/packet) to the server in order to modify the server privacy and waits
238     * for the answer.
239     * 
240     * @param requestPrivacy is the {@link Privacy} stanza(/packet) configured properly whose xml will be
241     *        sent to the server.
242     * @return a new {@link Privacy} with the data received from the server.
243     * @throws XMPPErrorException 
244     * @throws NoResponseException 
245     * @throws NotConnectedException 
246     */
247    private Stanza setRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException  {
248        // The request is a get iq type
249        requestPrivacy.setType(Privacy.Type.set);
250
251        return connection().createPacketCollectorAndSend(requestPrivacy).nextResultOrThrow();
252    }
253
254        /**
255         * Answer a privacy containing the list structure without {@link PrivacyItem}.
256         * 
257         * @return a Privacy with the list names.
258         * @throws XMPPErrorException 
259         * @throws NoResponseException 
260         * @throws NotConnectedException 
261         */ 
262        private Privacy getPrivacyWithListNames() throws NoResponseException, XMPPErrorException, NotConnectedException {
263                // The request of the list is an empty privacy message
264                Privacy request = new Privacy();
265                
266                // Send the package to the server and get the answer
267                return getRequest(request);
268        }
269
270    /**
271     * Answer the active privacy list. Returns <code>null</code> if there is no active list.
272     * 
273     * @return the privacy list of the active list.
274     * @throws XMPPErrorException 
275     * @throws NoResponseException 
276     * @throws NotConnectedException 
277     */ 
278    public PrivacyList getActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException  {
279        Privacy privacyAnswer = this.getPrivacyWithListNames();
280        String listName = privacyAnswer.getActiveName();
281        if (StringUtils.isNullOrEmpty(listName)) {
282            return null;
283        }
284        boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getDefaultName());
285        return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));
286    }
287
288    /**
289     * Get the name of the active list.
290     * 
291     * @return the name of the active list or null if there is none set.
292     * @throws NoResponseException
293     * @throws XMPPErrorException
294     * @throws NotConnectedException
295     * @since 4.1
296     */
297    public String getActiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
298        if (cachedActiveListName != null) {
299            return cachedActiveListName;
300        }
301        return getPrivacyWithListNames().getActiveName();
302    }
303
304    /**
305     * Answer the default privacy list. Returns <code>null</code> if there is no default list.
306     * 
307     * @return the privacy list of the default list.
308     * @throws XMPPErrorException 
309     * @throws NoResponseException 
310     * @throws NotConnectedException 
311     */ 
312    public PrivacyList getDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException {
313        Privacy privacyAnswer = this.getPrivacyWithListNames();
314        String listName = privacyAnswer.getDefaultName();
315        if (StringUtils.isNullOrEmpty(listName)) {
316            return null;
317        }
318        boolean isDefaultAndActive = listName.equals(privacyAnswer.getActiveName());
319        return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));
320    }
321
322    /**
323     * Get the name of the default list.
324     *
325     * @return the name of the default list or null if there is none set.
326     * @throws NoResponseException
327     * @throws XMPPErrorException
328     * @throws NotConnectedException
329     * @since 4.1
330     */
331    public String getDefaultListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
332        if (cachedDefaultListName != null) {
333            return cachedDefaultListName;
334        }
335        return getPrivacyWithListNames().getDefaultName();
336    }
337
338    /**
339     * Returns the name of the effective privacy list.
340     * <p>
341     * The effective privacy list is the one that is currently enforced on the connection. It's either the active
342     * privacy list, or, if the active privacy list is not set, the default privacy list.
343     * </p>
344     *
345     * @return the name of the effective privacy list or null if there is none set.
346     * @throws NoResponseException
347     * @throws XMPPErrorException
348     * @throws NotConnectedException
349     * @since 4.1
350     */
351    public String getEffectiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException {
352        String activeListName = getActiveListName();
353        if (activeListName != null) {
354            return activeListName;
355        }
356        return getDefaultListName();
357    }
358
359    /**
360     * Answer the privacy list items under listName with the allowed and blocked permissions.
361     * 
362     * @param listName the name of the list to get the allowed and blocked permissions.
363     * @return a list of privacy items under the list listName.
364     * @throws XMPPErrorException 
365     * @throws NoResponseException 
366     * @throws NotConnectedException 
367     */ 
368    private List<PrivacyItem> getPrivacyListItems(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException  {
369        assert StringUtils.isNotEmpty(listName);
370        // The request of the list is an privacy message with an empty list
371        Privacy request = new Privacy();
372        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
373        
374        // Send the package to the server and get the answer
375        Privacy privacyAnswer = getRequest(request);
376        
377        return privacyAnswer.getPrivacyList(listName);
378    }
379
380        /**
381         * Answer the privacy list items under listName with the allowed and blocked permissions.
382         * 
383         * @param listName the name of the list to get the allowed and blocked permissions.
384         * @return a privacy list under the list listName.
385         * @throws XMPPErrorException 
386         * @throws NoResponseException 
387         * @throws NotConnectedException 
388         */ 
389        public PrivacyList getPrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException  {
390        listName = StringUtils.requireNotNullOrEmpty(listName, "List name must not be null");
391        return new PrivacyList(false, false, listName, getPrivacyListItems(listName));
392        }
393
394    /**
395     * Answer every privacy list with the allowed and blocked permissions.
396     * 
397     * @return an array of privacy lists.
398     * @throws XMPPErrorException 
399     * @throws NoResponseException 
400     * @throws NotConnectedException 
401     */ 
402    public List<PrivacyList> getPrivacyLists() throws NoResponseException, XMPPErrorException, NotConnectedException {
403        Privacy privacyAnswer = getPrivacyWithListNames();
404        Set<String> names = privacyAnswer.getPrivacyListNames();
405        List<PrivacyList> lists = new ArrayList<>(names.size());
406        for (String listName : names) {
407            boolean isActiveList = listName.equals(privacyAnswer.getActiveName());
408            boolean isDefaultList = listName.equals(privacyAnswer.getDefaultName());
409            lists.add(new PrivacyList(isActiveList, isDefaultList, listName,
410                            getPrivacyListItems(listName)));
411        }
412        return lists;
413    }
414
415        /**
416         * Set or change the active list to listName.
417         * 
418         * @param listName the list name to set as the active one.
419         * @throws XMPPErrorException 
420         * @throws NoResponseException 
421         * @throws NotConnectedException 
422         */ 
423        public void setActiveListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException {
424                // The request of the list is an privacy message with an empty list
425                Privacy request = new Privacy();
426                request.setActiveName(listName);
427                
428                // Send the package to the server
429                setRequest(request);
430        }
431
432        /**
433         * Client declines the use of active lists.
434         * @throws XMPPErrorException 
435         * @throws NoResponseException 
436         * @throws NotConnectedException 
437         */ 
438        public void declineActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException {
439                // The request of the list is an privacy message with an empty list
440                Privacy request = new Privacy();
441                request.setDeclineActiveList(true);
442                
443                // Send the package to the server
444                setRequest(request);
445        }
446
447        /**
448         * Set or change the default list to listName.
449         * 
450         * @param listName the list name to set as the default one.
451         * @throws XMPPErrorException 
452         * @throws NoResponseException 
453         * @throws NotConnectedException 
454         */ 
455        public void setDefaultListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException  {
456                // The request of the list is an privacy message with an empty list
457                Privacy request = new Privacy();
458                request.setDefaultName(listName);
459                
460                // Send the package to the server
461                setRequest(request);
462        }
463
464        /**
465         * Client declines the use of default lists.
466         * @throws XMPPErrorException 
467         * @throws NoResponseException 
468         * @throws NotConnectedException 
469         */ 
470        public void declineDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException {
471                // The request of the list is an privacy message with an empty list
472                Privacy request = new Privacy();
473                request.setDeclineDefaultList(true);
474                
475                // Send the package to the server
476                setRequest(request);
477        }
478
479        /**
480         * The client has created a new list. It send the new one to the server.
481         * 
482     * @param listName the list that has changed its content.
483     * @param privacyItems a List with every privacy item in the list.
484         * @throws XMPPErrorException 
485         * @throws NoResponseException 
486         * @throws NotConnectedException 
487         */ 
488        public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException  {
489                updatePrivacyList(listName, privacyItems);
490        }
491
492    /**
493     * The client has edited an existing list. It updates the server content with the resulting 
494     * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the 
495     * list (not the "delta").
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 
500     * @throws NoResponseException 
501     * @throws NotConnectedException 
502     */ 
503    public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException  {
504        // Build the privacy package to add or update the new list
505        Privacy request = new Privacy();
506        request.setPrivacyList(listName, privacyItems);
507
508        // Send the package to the server
509        setRequest(request);
510    }
511
512        /**
513         * Remove a privacy list.
514         * 
515     * @param listName the list that has changed its content.
516         * @throws XMPPErrorException 
517         * @throws NoResponseException 
518         * @throws NotConnectedException 
519         */ 
520        public void deletePrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException {
521                // The request of the list is an privacy message with an empty list
522                Privacy request = new Privacy();
523                request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
524
525                // Send the package to the server
526                setRequest(request);
527        }
528
529    /**
530     * Adds a privacy list listener that will be notified of any new update in the user
531     * privacy communication.
532     *
533     * @param listener a privacy list listener.
534     * @return true, if the listener was not already added.
535     */
536    public boolean addListener(PrivacyListListener listener) {
537        return listeners.add(listener);
538    }
539
540    /**
541     * Removes the privacy list listener.
542     *
543     * @param listener
544     * @return true, if the listener was removed.
545     */
546    public boolean removeListener(PrivacyListListener listener) {
547        return listeners.remove(listener);
548    }
549
550    /**
551     * Check if the user's server supports privacy lists.
552     * 
553     * @return true, if the server supports privacy lists, false otherwise.
554     * @throws XMPPErrorException 
555     * @throws NoResponseException 
556     * @throws NotConnectedException 
557     */
558    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException{
559        return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE);
560    }
561}