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}