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 } 147 }, iqResultReplyFilter); 148 } 149 }, SetActiveListFilter.INSTANCE); 150 connection.addStanzaSendingListener(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 } 168 }, iqResultReplyFilter); 169 } 170 }, SetDefaultListFilter.INSTANCE); 171 connection.addSyncStanzaListener(new StanzaListener() { 172 @Override 173 public void processStanza(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 ConnectionListener() { 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 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 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 if there was an XMPP error returned. 226 * @throws NoResponseException if there was no response from the remote entity. 227 * @throws NotConnectedException if the XMPP connection is not connected. 228 * @throws InterruptedException if the calling thread was interrupted. 229 */ 230 private Privacy getRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 231 // The request is a get iq type 232 requestPrivacy.setType(Privacy.Type.get); 233 234 return connection().sendIqRequestAndWaitForResponse(requestPrivacy); 235 } 236 237 /** 238 * Send the {@link Privacy} stanza to the server in order to modify the server privacy and waits 239 * for the answer. 240 * 241 * @param requestPrivacy is the {@link Privacy} stanza configured properly whose xml will be 242 * sent to the server. 243 * @return a new {@link Privacy} with the data received from the server. 244 * @throws XMPPErrorException if there was an XMPP error returned. 245 * @throws NoResponseException if there was no response from the remote entity. 246 * @throws NotConnectedException if the XMPP connection is not connected. 247 * @throws InterruptedException if the calling thread was interrupted. 248 */ 249 private Stanza setRequest(Privacy requestPrivacy) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 250 // The request is a get iq type 251 requestPrivacy.setType(Privacy.Type.set); 252 253 return connection().sendIqRequestAndWaitForResponse(requestPrivacy); 254 } 255 256 /** 257 * Answer a privacy containing the list structure without {@link PrivacyItem}. 258 * 259 * @return a Privacy with the list names. 260 * @throws XMPPErrorException if there was an XMPP error returned. 261 * @throws NoResponseException if there was no response from the remote entity. 262 * @throws NotConnectedException if the XMPP connection is not connected. 263 * @throws InterruptedException if the calling thread was interrupted. 264 */ 265 private Privacy getPrivacyWithListNames() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 266 // The request of the list is an empty privacy message 267 Privacy request = new Privacy(); 268 269 // Send the package to the server and get the answer 270 return getRequest(request); 271 } 272 273 /** 274 * Answer the active privacy list. Returns <code>null</code> if there is no active list. 275 * 276 * @return the privacy list of the active list. 277 * @throws XMPPErrorException if there was an XMPP error returned. 278 * @throws NoResponseException if there was no response from the remote entity. 279 * @throws NotConnectedException if the XMPP connection is not connected. 280 * @throws InterruptedException if the calling thread was interrupted. 281 */ 282 public PrivacyList getActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 283 Privacy privacyAnswer = this.getPrivacyWithListNames(); 284 String listName = privacyAnswer.getActiveName(); 285 if (StringUtils.isNullOrEmpty(listName)) { 286 return null; 287 } 288 boolean isDefaultAndActive = listName != null && listName.equals(privacyAnswer.getDefaultName()); 289 return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName)); 290 } 291 292 /** 293 * Get the name of the active list. 294 * 295 * @return the name of the active list or null if there is none set. 296 * @throws NoResponseException if there was no response from the remote entity. 297 * @throws XMPPErrorException if there was an XMPP error returned. 298 * @throws NotConnectedException if the XMPP connection is not connected. 299 * @throws InterruptedException if the calling thread was interrupted. 300 * @since 4.1 301 */ 302 public String getActiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 303 if (cachedActiveListName != null) { 304 return cachedActiveListName; 305 } 306 return getPrivacyWithListNames().getActiveName(); 307 } 308 309 /** 310 * Answer the default privacy list. Returns <code>null</code> if there is no default list. 311 * 312 * @return the privacy list of the default list. 313 * @throws XMPPErrorException if there was an XMPP error returned. 314 * @throws NoResponseException if there was no response from the remote entity. 315 * @throws NotConnectedException if the XMPP connection is not connected. 316 * @throws InterruptedException if the calling thread was interrupted. 317 */ 318 public PrivacyList getDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 319 Privacy privacyAnswer = this.getPrivacyWithListNames(); 320 String listName = privacyAnswer.getDefaultName(); 321 if (StringUtils.isNullOrEmpty(listName)) { 322 return null; 323 } 324 boolean isDefaultAndActive = listName.equals(privacyAnswer.getActiveName()); 325 return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName)); 326 } 327 328 /** 329 * Get the name of the default list. 330 * 331 * @return the name of the default list or null if there is none set. 332 * @throws NoResponseException if there was no response from the remote entity. 333 * @throws XMPPErrorException if there was an XMPP error returned. 334 * @throws NotConnectedException if the XMPP connection is not connected. 335 * @throws InterruptedException if the calling thread was interrupted. 336 * @since 4.1 337 */ 338 public String getDefaultListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 339 if (cachedDefaultListName != null) { 340 return cachedDefaultListName; 341 } 342 return getPrivacyWithListNames().getDefaultName(); 343 } 344 345 /** 346 * Returns the name of the effective privacy list. 347 * <p> 348 * The effective privacy list is the one that is currently enforced on the connection. It's either the active 349 * privacy list, or, if the active privacy list is not set, the default privacy list. 350 * </p> 351 * 352 * @return the name of the effective privacy list or null if there is none set. 353 * @throws NoResponseException if there was no response from the remote entity. 354 * @throws XMPPErrorException if there was an XMPP error returned. 355 * @throws NotConnectedException if the XMPP connection is not connected. 356 * @throws InterruptedException if the calling thread was interrupted. 357 * @since 4.1 358 */ 359 public String getEffectiveListName() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 360 String activeListName = getActiveListName(); 361 if (activeListName != null) { 362 return activeListName; 363 } 364 return getDefaultListName(); 365 } 366 367 /** 368 * Answer the privacy list items under listName with the allowed and blocked permissions. 369 * 370 * @param listName the name of the list to get the allowed and blocked permissions. 371 * @return a list of privacy items under the list listName. 372 * @throws XMPPErrorException if there was an XMPP error returned. 373 * @throws NoResponseException if there was no response from the remote entity. 374 * @throws NotConnectedException if the XMPP connection is not connected. 375 * @throws InterruptedException if the calling thread was interrupted. 376 */ 377 private List<PrivacyItem> getPrivacyListItems(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 378 assert StringUtils.isNotEmpty(listName); 379 // The request of the list is an privacy message with an empty list 380 Privacy request = new Privacy(); 381 request.setPrivacyList(listName, new ArrayList<PrivacyItem>()); 382 383 // Send the package to the server and get the answer 384 Privacy privacyAnswer = getRequest(request); 385 386 return privacyAnswer.getPrivacyList(listName); 387 } 388 389 /** 390 * Answer the privacy list items under listName with the allowed and blocked permissions. 391 * 392 * @param listName the name of the list to get the allowed and blocked permissions. 393 * @return a privacy list under the list listName. 394 * @throws XMPPErrorException if there was an XMPP error returned. 395 * @throws NoResponseException if there was no response from the remote entity. 396 * @throws NotConnectedException if the XMPP connection is not connected. 397 * @throws InterruptedException if the calling thread was interrupted. 398 */ 399 public PrivacyList getPrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 400 listName = StringUtils.requireNotNullNorEmpty(listName, "List name must not be null"); 401 return new PrivacyList(false, false, listName, getPrivacyListItems(listName)); 402 } 403 404 /** 405 * Answer every privacy list with the allowed and blocked permissions. 406 * 407 * @return an array of privacy lists. 408 * @throws XMPPErrorException if there was an XMPP error returned. 409 * @throws NoResponseException if there was no response from the remote entity. 410 * @throws NotConnectedException if the XMPP connection is not connected. 411 * @throws InterruptedException if the calling thread was interrupted. 412 */ 413 public List<PrivacyList> getPrivacyLists() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 414 Privacy privacyAnswer = getPrivacyWithListNames(); 415 Set<String> names = privacyAnswer.getPrivacyListNames(); 416 List<PrivacyList> lists = new ArrayList<>(names.size()); 417 for (String listName : names) { 418 boolean isActiveList = listName.equals(privacyAnswer.getActiveName()); 419 boolean isDefaultList = listName.equals(privacyAnswer.getDefaultName()); 420 lists.add(new PrivacyList(isActiveList, isDefaultList, listName, 421 getPrivacyListItems(listName))); 422 } 423 return lists; 424 } 425 426 /** 427 * Set or change the active list to listName. 428 * 429 * @param listName the list name to set as the active one. 430 * @throws XMPPErrorException if there was an XMPP error returned. 431 * @throws NoResponseException if there was no response from the remote entity. 432 * @throws NotConnectedException if the XMPP connection is not connected. 433 * @throws InterruptedException if the calling thread was interrupted. 434 */ 435 public void setActiveListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 436 // The request of the list is an privacy message with an empty list 437 Privacy request = new Privacy(); 438 request.setActiveName(listName); 439 440 // Send the package to the server 441 setRequest(request); 442 } 443 444 /** 445 * Client declines the use of active lists. 446 * @throws XMPPErrorException if there was an XMPP error returned. 447 * @throws NoResponseException if there was no response from the remote entity. 448 * @throws NotConnectedException if the XMPP connection is not connected. 449 * @throws InterruptedException if the calling thread was interrupted. 450 */ 451 public void declineActiveList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 452 // The request of the list is an privacy message with an empty list 453 Privacy request = new Privacy(); 454 request.setDeclineActiveList(true); 455 456 // Send the package to the server 457 setRequest(request); 458 } 459 460 /** 461 * Set or change the default list to listName. 462 * 463 * @param listName the list name to set as the default one. 464 * @throws XMPPErrorException if there was an XMPP error returned. 465 * @throws NoResponseException if there was no response from the remote entity. 466 * @throws NotConnectedException if the XMPP connection is not connected. 467 * @throws InterruptedException if the calling thread was interrupted. 468 */ 469 public void setDefaultListName(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 470 // The request of the list is an privacy message with an empty list 471 Privacy request = new Privacy(); 472 request.setDefaultName(listName); 473 474 // Send the package to the server 475 setRequest(request); 476 } 477 478 /** 479 * Client declines the use of default lists. 480 * @throws XMPPErrorException if there was an XMPP error returned. 481 * @throws NoResponseException if there was no response from the remote entity. 482 * @throws NotConnectedException if the XMPP connection is not connected. 483 * @throws InterruptedException if the calling thread was interrupted. 484 */ 485 public void declineDefaultList() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 486 // The request of the list is an privacy message with an empty list 487 Privacy request = new Privacy(); 488 request.setDeclineDefaultList(true); 489 490 // Send the package to the server 491 setRequest(request); 492 } 493 494 /** 495 * The client has created a new list. It send the new one to the server. 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 if there was an XMPP error returned. 500 * @throws NoResponseException if there was no response from the remote entity. 501 * @throws NotConnectedException if the XMPP connection is not connected. 502 * @throws InterruptedException if the calling thread was interrupted. 503 */ 504 public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 505 updatePrivacyList(listName, privacyItems); 506 } 507 508 /** 509 * The client has edited an existing list. It updates the server content with the resulting 510 * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the 511 * list (not the "delta"). 512 * 513 * @param listName the list that has changed its content. 514 * @param privacyItems a List with every privacy item in the list. 515 * @throws XMPPErrorException if there was an XMPP error returned. 516 * @throws NoResponseException if there was no response from the remote entity. 517 * @throws NotConnectedException if the XMPP connection is not connected. 518 * @throws InterruptedException if the calling thread was interrupted. 519 */ 520 public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 521 // Build the privacy package to add or update the new list 522 Privacy request = new Privacy(); 523 request.setPrivacyList(listName, privacyItems); 524 525 // Send the package to the server 526 setRequest(request); 527 } 528 529 /** 530 * Remove a privacy list. 531 * 532 * @param listName the list that has changed its content. 533 * @throws XMPPErrorException if there was an XMPP error returned. 534 * @throws NoResponseException if there was no response from the remote entity. 535 * @throws NotConnectedException if the XMPP connection is not connected. 536 * @throws InterruptedException if the calling thread was interrupted. 537 */ 538 public void deletePrivacyList(String listName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 539 // The request of the list is an privacy message with an empty list 540 Privacy request = new Privacy(); 541 request.setPrivacyList(listName, new ArrayList<PrivacyItem>()); 542 543 // Send the package to the server 544 setRequest(request); 545 } 546 547 /** 548 * Adds a privacy list listener that will be notified of any new update in the user 549 * privacy communication. 550 * 551 * @param listener a privacy list listener. 552 * @return true, if the listener was not already added. 553 */ 554 public boolean addListener(PrivacyListListener listener) { 555 return listeners.add(listener); 556 } 557 558 /** 559 * Removes the privacy list listener. 560 * 561 * @param listener TODO javadoc me please 562 * @return true, if the listener was removed. 563 */ 564 public boolean removeListener(PrivacyListListener listener) { 565 return listeners.remove(listener); 566 } 567 568 /** 569 * Check if the user's server supports privacy lists. 570 * 571 * @return true, if the server supports privacy lists, false otherwise. 572 * @throws XMPPErrorException if there was an XMPP error returned. 573 * @throws NoResponseException if there was no response from the remote entity. 574 * @throws NotConnectedException if the XMPP connection is not connected. 575 * @throws InterruptedException if the calling thread was interrupted. 576 */ 577 public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 578 return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE); 579 } 580}