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