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