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}