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